feat(android-sqlite): Add SentrySQLiteDriver (JAVA-275)#5466
feat(android-sqlite): Add SentrySQLiteDriver (JAVA-275)#54660xadam-brown wants to merge 3 commits into
Conversation
|
📲 Install BuildsAndroid
|
Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| ad8da22 | 365.86 ms | 427.00 ms | 61.14 ms |
| abfcc92 | 337.38 ms | 427.39 ms | 90.00 ms |
| b8bd880 | 314.56 ms | 336.50 ms | 21.94 ms |
| 44472da | 313.96 ms | 365.35 ms | 51.39 ms |
| b03edbb | 352.20 ms | 423.69 ms | 71.49 ms |
| f6cdbf0 | 314.19 ms | 357.59 ms | 43.40 ms |
| f064536 | 329.00 ms | 395.62 ms | 66.62 ms |
| 806307f | 357.85 ms | 424.64 ms | 66.79 ms |
| d15471f | 343.13 ms | 361.47 ms | 18.34 ms |
| 6edfca2 | 316.43 ms | 398.90 ms | 82.46 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| ad8da22 | 1.58 MiB | 2.29 MiB | 719.83 KiB |
| abfcc92 | 1.58 MiB | 2.13 MiB | 557.31 KiB |
| b8bd880 | 1.58 MiB | 2.29 MiB | 722.92 KiB |
| 44472da | 0 B | 0 B | 0 B |
| b03edbb | 1.58 MiB | 2.13 MiB | 557.32 KiB |
| f6cdbf0 | 0 B | 0 B | 0 B |
| f064536 | 1.58 MiB | 2.20 MiB | 633.90 KiB |
| 806307f | 1.58 MiB | 2.10 MiB | 533.42 KiB |
| d15471f | 1.58 MiB | 2.13 MiB | 559.54 KiB |
| 6edfca2 | 1.58 MiB | 2.13 MiB | 559.07 KiB |
Previous results on branch: feat/support-sqlite-driver
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 2a55b58 | 365.41 ms | 434.64 ms | 69.23 ms |
| f8d2380 | 309.09 ms | 365.52 ms | 56.43 ms |
| 4993a1b | 303.45 ms | 392.65 ms | 89.20 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 2a55b58 | 0 B | 0 B | 0 B |
| f8d2380 | 0 B | 0 B | 0 B |
| 4993a1b | 0 B | 0 B | 0 B |
a590992 to
0084312
Compare
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>
4c87cb9 to
4c67ea9
Compare
Method reference `System::nanoTime` compiles to FunctionReferenceImpl, which breaks R8 in the SDK size test app.
markushi
left a comment
There was a problem hiding this comment.
Looks very promising already, left a few comments. Once we've decided what execution phases spans should cover, I'll have a second look.
| } catch (e: Throwable) { | ||
| span = scopes.span?.startChild("db.sql.query", sql, startTimestamp, Instrumenter.SENTRY) | ||
| span?.spanContext?.origin = TRACE_ORIGIN | ||
| span = spanHelper.startSpan(sql, startTimestamp) |
There was a problem hiding this comment.
wait, I think this doesn't make sense, or does it? If an sql operation fails we start a new span... for throwing an exception? I guess this was overlooked, we should drop this 😅
There was a problem hiding this comment.
ah, dammit it had been introduced during merge conflict resolution. We should def. drop this
There was a problem hiding this comment.
Hmm... I actually think this was intentional. (It's been with us for a while and it's a pattern used elsewhere when an action that should produce a plain vanilla span throws instead, see eg sentry-jdbc, sentry-okhttp, etc.)
Note that spanHelper uses a lazy start pattern here, in that it's documenting the error span after the fact and immediately finishes the span in finally, rather than leaving it open.
So presumably we want to keep this? (+ the similar INTERNAL_ERROR span in the catch clause of the new SentrySQLiteStatement.span() method).
That said, let me know if I goofed and we really do want to strip the error span out.
There was a problem hiding this comment.
ah, indeed, I missed that we're using the startTimestamp that we retrieved at the beginning of the method 🙈
I guess it reads a bit confusing (like we're starting the span inside catch {} as opposed to starting it before the sql op), I'd expect something like:
val span = spanHelper.startSpan(sql, now)
try {
val result = operation()
span?.status = SpanStatus.OK
result
} catch (e: Throwable) {
span?.status = INTERNAL_ERROR
span?.throwable = e
} finally {
...
span?.finish()
}which is more natural. But the current approach is also fine by me 👍
There was a problem hiding this comment.
Yeah, this is due to my ambition to share span building logic btw the open helper and driver. I'll see whether I can't make the API more obvious.
There was a problem hiding this comment.
Updated to avoid splitting between startSpan() and finishSpan() calls. Now there's just one w/a (hopefully) less misleading name:
@@ -43,19 +41,11 @@ internal class SQLiteSpanManager(
if (result is CrossProcessCursor) {
return SentryCrossProcessCursor(result, this, sql) as T
}
- span = spanHelper.startSpan(sql, startTimestamp)
- span?.status = SpanStatus.OK
+ spans.recordSpan(sql, startTimestamp, SpanStatus.OK)
result
} catch (e: Throwable) {
- span = spanHelper.startSpan(sql, startTimestamp)
- span?.status = SpanStatus.INTERNAL_ERROR
- span?.throwable = e
+ spans.recordSpan(sql, startTimestamp, SpanStatus.INTERNAL_ERROR, e)
throw e
- } finally {
- span?.let {
- spanHelper.applyDataToSpan(it)
- it.finish()
- }
}
}
}There was a problem hiding this comment.
I really like that you've tried to split responsibilities here, but was wondering if we could somehow reduce the number of helper classes? 😅 We now have a Manager, a Recorder and a Helper -- that's too many "ers" for my taste, haha, and it's a bit difficult to make a distinction between them.
I understand that it might be not possible due to backwards-compat with the existing sqlite instrumentation, but perhaps there's a way?
There was a problem hiding this comment.
Two options (chime in if you prefer a third):
- Merge
SQLiteSpanHelperintoSQLiteSpanRecorderat the cost of making the resulting API wider than it needs to be for either consumer. - Do (1) but no longer share logic across the
.android.sqliteand.sqlitepackages (lets us keep theSQLiteSpanRecorderAPI tailored to its now sole consumer).
I'll do (1), see whether (2) calls to me, and then make a decision on my end. Feel free to wait for me to post my next commit if you don't have a strong opinion in the meantime....
There was a problem hiding this comment.
Did (1) and it was able to tailor the API appropriately to each consumer by overloading a single recordSpan() method. Satisfied on my end. Let me know if you'd like any additional changes.
romtsn
left a comment
There was a problem hiding this comment.
Looks great, nice analysis and research, kudos!
Some more things I'd want to clarify before approving:
- do we actually need to bump
androidx.sqliteversion 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)
|
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 Not for this PR: we're already on androidx.sqlite:sqlite:2.5.2 and But we will need to bump to 2.7.0 in my follow-on PR for fixing the Room 3.0-alpha error discussed offline (error is caused by how D8 de-sugars the recently-introduced 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 u This ended up being an important question (thx!). I propose keeping the driver in the existing Here are the migration steps I had in mind:
// 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.
cc @markushi Step-by-step migration path to KMP: Click to expandMigration path
User experience
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 |
- Merge SQLiteSpanHelper + SQLiteSpanRecorder into a single SQLiteSpanInstrumentation class. - DRY out reference to file name path separators.
📜 Description
Introduces
SentrySQLiteDriverfor wrapping and instrumentingandroidx.sqlite.SQLiteDriver. Like our existingSentrySupportSQLiteOpenHelper, the new wrapper produces a span per executed SQL statement.Example use:
💡 Motivation and Context
Room 2.7 introduced the
SQLiteDriverAPI as a replacement forSupportSQLiteOpenHelper; Room 3.0+ makes use ofSQLiteDrivermandatory.A key motivation behind Google's introduction of
SQLiteDriverwas Kotlin Multiplatform compatibility. This PR makesSentrySQLiteDriveravailable 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.
[1] Unlike
SentrySupportSQLiteOpenHelper, theSentrySQLiteDriveris 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: acreate(SQLiteDriver)constructor; future additions are non-breaking; sufficient alignment withSentrySupportSQLiteOpenHelperdata model. That said, chime in if you think we should add it.)[3] Behavior differences vs
SentrySupportSQLiteHelperClick to expand
Behavior differences
SentrySupportSQLiteOpenHelper(old)SentrySQLiteDriver(new)performSqlcall (incl. app time during cursor materialization)step()db time only – app time between steps excludedgetCount/onMove/fillWindowtimed; later rows untimedstep()contributes to cumulative span timestep()of the cycledb.namederivationdatabaseName(for Room, the builder name, e.g.,"tracks"fromdatabaseBuilder(ctx, MyDb, "tracks")).File(fileName).name— the on-disk filename of the path Room passes todriver.open()(e.g.,"tracks.db").db.namevalues during migration. Both paths report data correctly, but will attribute it to different sources.execSQL("CREATE TABLE …; INSERT …; INSERT …;")produces one span whose description is the full script.prepare(...), so multi-statement scripts must be split by Room (or the caller) into separate prepare/step cycles → one span per statement.[4] Risk of duplicate spans with Room's bridge adapter
SentrySQLiteDriverprotects 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 andSentrySupportSQLiteOpenHelperare 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 aSQLiteDriverfrom an existingSupportSQLiteOpenHelper. 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 existingSupportSQLiteOpenHelperto aSQLiteDriverthat 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:💚 How did you test it?
Unit tests cover:
SentrySQLiteDriverdelegation and wrappingSentrySQLiteConnectiondelegation and wrappingSentrySQLiteStatementspan creation, cancellation, and success/error taggingSQLiteSpanRecorderspan lifecycle (start/finish/cancel)I also dog-fooded
SentrySQLiteDriveron my own example app via Maven local artifact + I verified spans are displayed in Sentry UI.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
sendDefaultPIIis enabled.🔮 Next steps
SQLiteDrivervia the sentry-android-gradle-plugin at some point (not currently on the roadmap).