From be3834c518ca21eeda2e327efc62af2cafe5a1f2 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 17 Jul 2026 11:45:40 +0200 Subject: [PATCH] feat(database): Apply Data Collection query policy Suppress SQL statement descriptions when database query data collection is disabled while retaining database system, name, timing, status, and other structural span metadata. Preserve existing statement collection when Data Collection is absent. Co-Authored-By: Claude --- .../sentry/android/sqlite/OpenHelperSpans.kt | 13 +++++++++-- .../main/java/io/sentry/sqlite/DriverSpans.kt | 5 +++- .../android/sqlite/OpenHelperSpansTest.kt | 22 ++++++++++++++++++ .../java/io/sentry/sqlite/DriverSpansTest.kt | 22 ++++++++++++++++++ .../sentry/jdbc/SentryJdbcEventListener.java | 6 ++++- .../jdbc/SentryJdbcEventListenerTest.kt | 23 +++++++++++++++++++ sentry/api/sentry.api | 1 + .../io/sentry/DataCollectionResolver.java | 4 ++++ .../io/sentry/DataCollectionResolverTest.kt | 11 +++++++++ 9 files changed, 103 insertions(+), 4 deletions(-) diff --git a/sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/OpenHelperSpans.kt b/sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/OpenHelperSpans.kt index 059eb1bb1b5..4fe75ef4d28 100644 --- a/sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/OpenHelperSpans.kt +++ b/sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/OpenHelperSpans.kt @@ -6,6 +6,7 @@ import io.sentry.IScopes import io.sentry.ISpan import io.sentry.Instrumenter import io.sentry.ScopesAdapter +import io.sentry.SentryDate import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryStackTraceFactory import io.sentry.SpanDataConvention @@ -46,12 +47,12 @@ internal class OpenHelperSpans( if (result is CrossProcessCursor) { return SentryCrossProcessCursor(result, this, sql) as T } - span = scopes.span?.startChild("db.sql.query", sql, startTimestamp, Instrumenter.SENTRY) + span = startSpan(sql, startTimestamp) span?.spanContext?.origin = TRACE_ORIGIN span?.status = SpanStatus.OK result } catch (e: Throwable) { - span = scopes.span?.startChild("db.sql.query", sql, startTimestamp, Instrumenter.SENTRY) + span = startSpan(sql, startTimestamp) span?.spanContext?.origin = TRACE_ORIGIN span?.status = SpanStatus.INTERNAL_ERROR span?.throwable = e @@ -76,4 +77,12 @@ internal class OpenHelperSpans( } } } + + private fun startSpan(sql: String, startTimestamp: SentryDate): ISpan? = + scopes.span?.startChild( + "db.sql.query", + sql.takeIf { scopes.options.dataCollectionResolver.isDatabaseQueryDataWithLegacyAlways }, + startTimestamp, + Instrumenter.SENTRY, + ) } diff --git a/sentry-android-sqlite/src/main/java/io/sentry/sqlite/DriverSpans.kt b/sentry-android-sqlite/src/main/java/io/sentry/sqlite/DriverSpans.kt index b3c0eb7c713..fe2b15a33bb 100644 --- a/sentry-android-sqlite/src/main/java/io/sentry/sqlite/DriverSpans.kt +++ b/sentry-android-sqlite/src/main/java/io/sentry/sqlite/DriverSpans.kt @@ -50,7 +50,10 @@ internal class DriverSpans(private val scopes: IScopes, private val dbMetadata: val startTimestamp = SentryLongDate(startTimestampNanos) val endTimestamp = SentryLongDate(startTimestampNanos + durationNanos) - parent.startChild("db.sql.query", sql, startTimestamp, Instrumenter.SENTRY).apply { + val description = sql.takeIf { + scopes.options.dataCollectionResolver.isDatabaseQueryDataWithLegacyAlways + } + parent.startChild("db.sql.query", description, startTimestamp, Instrumenter.SENTRY).apply { spanContext.origin = SQLITE_TRACE_ORIGIN throwable?.let { this.throwable = it } diff --git a/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/OpenHelperSpansTest.kt b/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/OpenHelperSpansTest.kt index 0552094838e..8b442c59ee5 100644 --- a/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/OpenHelperSpansTest.kt +++ b/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/OpenHelperSpansTest.kt @@ -66,6 +66,28 @@ class OpenHelperSpansTest { assertTrue(span.isFinished) } + @Test + fun `performSql omits description when database query data is disabled`() { + val sut = fixture.getSut() + fixture.options.dataCollection.setDatabaseQueryData(false) + + sut.performSql("SELECT secret FROM users") {} + + val span = fixture.sentryTracer.children.first() + assertNull(span.description) + assertEquals("in-memory", span.data[SpanDataConvention.DB_SYSTEM_KEY]) + } + + @Test + fun `performSql keeps description in legacy mode`() { + val sut = fixture.getSut() + fixture.options.isSendDefaultPii = false + + sut.performSql("SELECT secret FROM users") {} + + assertEquals("SELECT secret FROM users", fixture.sentryTracer.children.first().description) + } + @Test fun `performSql does not create a span if no span is running`() { val sut = fixture.getSut(isSpanActive = false) diff --git a/sentry-android-sqlite/src/test/java/io/sentry/sqlite/DriverSpansTest.kt b/sentry-android-sqlite/src/test/java/io/sentry/sqlite/DriverSpansTest.kt index 319fc20d7ce..2265d10aa75 100644 --- a/sentry-android-sqlite/src/test/java/io/sentry/sqlite/DriverSpansTest.kt +++ b/sentry-android-sqlite/src/test/java/io/sentry/sqlite/DriverSpansTest.kt @@ -129,6 +129,28 @@ class DriverSpansTest { assertTrue(span.isFinished) } + @Test + fun `record method omits description when database query data is disabled`() { + val sut = fixture.getSut() + fixture.options.dataCollection.setDatabaseQueryData(false) + + sut.record("SELECT secret FROM users", sut.startTimestamp(), 1_000_000, SpanStatus.OK) + + val span = fixture.sentryTracer.children.first() + assertNull(span.description) + assertEquals("in-memory", span.data[SpanDataConvention.DB_SYSTEM_KEY]) + } + + @Test + fun `record method keeps description in legacy mode`() { + val sut = fixture.getSut() + fixture.options.isSendDefaultPii = false + + sut.record("SELECT secret FROM users", sut.startTimestamp(), 1_000_000, SpanStatus.OK) + + assertEquals("SELECT secret FROM users", fixture.sentryTracer.children.first().description) + } + @Test fun `record method sets finishDate equal to startDate + durationNanos`() { val sut = fixture.getSut() diff --git a/sentry-jdbc/src/main/java/io/sentry/jdbc/SentryJdbcEventListener.java b/sentry-jdbc/src/main/java/io/sentry/jdbc/SentryJdbcEventListener.java index 4206de18002..59e50efae26 100644 --- a/sentry-jdbc/src/main/java/io/sentry/jdbc/SentryJdbcEventListener.java +++ b/sentry-jdbc/src/main/java/io/sentry/jdbc/SentryJdbcEventListener.java @@ -47,7 +47,11 @@ public SentryJdbcEventListener() { @Override public void onBeforeAnyExecute(final @NotNull StatementInformation statementInformation) { - startSpan(CURRENT_QUERY_SPAN, "db.query", statementInformation.getSql()); + final @Nullable String description = + scopes.getOptions().getDataCollectionResolver().isDatabaseQueryDataWithLegacyAlways() + ? statementInformation.getSql() + : null; + startSpan(CURRENT_QUERY_SPAN, "db.query", description); } @Override diff --git a/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/SentryJdbcEventListenerTest.kt b/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/SentryJdbcEventListenerTest.kt index 22ee97e5d47..436bc4abf62 100644 --- a/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/SentryJdbcEventListenerTest.kt +++ b/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/SentryJdbcEventListenerTest.kt @@ -90,6 +90,29 @@ class SentryJdbcEventListenerTest { assertEquals("INSERT INTO foo VALUES (2)", fixture.tx.children[1].description) } + @Test + fun `omits query description when database query data is disabled`() { + val sut = fixture.getSut() + fixture.options.dataCollection.setDatabaseQueryData(false) + + sut.connection.use { it.prepareStatement("INSERT INTO foo VALUES (1)").executeUpdate() } + + assertEquals(1, fixture.tx.children.size) + assertEquals(null, fixture.tx.children.first().description) + assertEquals("hsqldb", fixture.tx.children.first().data[DB_SYSTEM_KEY]) + assertEquals("testdb", fixture.tx.children.first().data[DB_NAME_KEY]) + } + + @Test + fun `legacy mode keeps query description when sendDefaultPii is false`() { + val sut = fixture.getSut() + fixture.options.isSendDefaultPii = false + + sut.connection.use { it.prepareStatement("INSERT INTO foo VALUES (1)").executeUpdate() } + + assertEquals("INSERT INTO foo VALUES (1)", fixture.tx.children.first().description) + } + @Test fun `creates spans for calls resulting in error`() { val sut = fixture.getSut(existingRow = 1) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 4ab82709159..db9547700dc 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -424,6 +424,7 @@ public final class io/sentry/DataCollectionResolver { public fun getQueryParams ()Lio/sentry/KeyValueCollectionBehavior; public fun isDataCollectionConfigured ()Z public fun isDatabaseQueryData ()Z + public fun isDatabaseQueryDataWithLegacyAlways ()Z public fun isGraphqlDocument ()Z public fun isGraphqlDocumentWithLegacyAlways ()Z public fun isGraphqlDocumentWithLegacyBodyGate ()Z diff --git a/sentry/src/main/java/io/sentry/DataCollectionResolver.java b/sentry/src/main/java/io/sentry/DataCollectionResolver.java index cdfb0649188..a293614eb2b 100644 --- a/sentry/src/main/java/io/sentry/DataCollectionResolver.java +++ b/sentry/src/main/java/io/sentry/DataCollectionResolver.java @@ -31,6 +31,10 @@ public boolean isDatabaseQueryData() { return explicitOrSendDefaultPii(options.getDataCollection().getDatabaseQueryData(), true); } + public boolean isDatabaseQueryDataWithLegacyAlways() { + return explicitOrDefault(options.getDataCollection().getDatabaseQueryData(), true, true); + } + public boolean isGraphqlDocument() { return explicitOrSendDefaultPii(options.getDataCollection().getGraphql().getDocument(), true); } diff --git a/sentry/src/test/java/io/sentry/DataCollectionResolverTest.kt b/sentry/src/test/java/io/sentry/DataCollectionResolverTest.kt index 47f637f0a08..fb525ce43d2 100644 --- a/sentry/src/test/java/io/sentry/DataCollectionResolverTest.kt +++ b/sentry/src/test/java/io/sentry/DataCollectionResolverTest.kt @@ -78,6 +78,17 @@ class DataCollectionResolverTest { assertThat(options.dataCollectionResolver.isDatabaseQueryData).isFalse() } + @Test + fun `database query data legacy always variant preserves collection when namespace is absent`() { + val options = SentryOptions().apply { isSendDefaultPii = false } + + assertThat(options.dataCollectionResolver.isDatabaseQueryDataWithLegacyAlways).isTrue() + + options.dataCollection.setDatabaseQueryData(false) + + assertThat(options.dataCollectionResolver.isDatabaseQueryDataWithLegacyAlways).isFalse() + } + @Test fun `GraphQL document falls back to sendDefaultPii and override takes precedence`() { val options = SentryOptions().apply { isSendDefaultPii = true }