From c99f84982959ea6510237efee909d238b5f062cd Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 11 Aug 2023 10:51:51 +0200 Subject: [PATCH 1/7] Send db.system and db.name in span data --- sentry-jdbc/api/sentry-jdbc.api | 11 + sentry-jdbc/build.gradle.kts | 1 + .../java/io/sentry/jdbc/DatabaseUtils.java | 150 +++++++++++++ .../sentry/jdbc/SentryJdbcEventListener.java | 36 ++++ .../io/sentry/jdbc/DatabaseUtilsTest.kt | 199 ++++++++++++++++++ .../jdbc/SentryJdbcEventListenerTest.kt | 46 ++++ sentry/api/sentry.api | 4 + .../java/io/sentry/SpanDataConvention.java | 2 + .../main/java/io/sentry/util/StringUtils.java | 26 +++ .../java/io/sentry/util/StringUtilsTest.kt | 40 ++++ 10 files changed, 515 insertions(+) create mode 100644 sentry-jdbc/src/main/java/io/sentry/jdbc/DatabaseUtils.java create mode 100644 sentry-jdbc/src/test/kotlin/io/sentry/jdbc/DatabaseUtilsTest.kt diff --git a/sentry-jdbc/api/sentry-jdbc.api b/sentry-jdbc/api/sentry-jdbc.api index a1df685bc38..cff0f37fd26 100644 --- a/sentry-jdbc/api/sentry-jdbc.api +++ b/sentry-jdbc/api/sentry-jdbc.api @@ -3,6 +3,17 @@ public final class io/sentry/jdbc/BuildConfig { public static final field VERSION_NAME Ljava/lang/String; } +public final class io/sentry/jdbc/DatabaseUtils { + public fun ()V + public static fun parse (Ljava/lang/String;)Lio/sentry/jdbc/DatabaseUtils$DatabaseDetails; + public static fun readFrom (Lcom/p6spy/engine/common/StatementInformation;)Lio/sentry/jdbc/DatabaseUtils$DatabaseDetails; +} + +public final class io/sentry/jdbc/DatabaseUtils$DatabaseDetails { + public fun getDbName ()Ljava/lang/String; + public fun getDbSystem ()Ljava/lang/String; +} + public class io/sentry/jdbc/SentryJdbcEventListener : com/p6spy/engine/event/SimpleJdbcEventListener { public fun ()V public fun (Lio/sentry/IHub;)V diff --git a/sentry-jdbc/build.gradle.kts b/sentry-jdbc/build.gradle.kts index 0395470b8d6..239bd46cabc 100644 --- a/sentry-jdbc/build.gradle.kts +++ b/sentry-jdbc/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { testImplementation(kotlin(Config.kotlinStdLib)) testImplementation(Config.TestLibs.kotlinTestJunit) testImplementation(Config.TestLibs.mockitoKotlin) + testImplementation(Config.TestLibs.mockitoInline) testImplementation(Config.TestLibs.awaitility) testImplementation(Config.TestLibs.hsqldb) } diff --git a/sentry-jdbc/src/main/java/io/sentry/jdbc/DatabaseUtils.java b/sentry-jdbc/src/main/java/io/sentry/jdbc/DatabaseUtils.java new file mode 100644 index 00000000000..14524c67e2e --- /dev/null +++ b/sentry-jdbc/src/main/java/io/sentry/jdbc/DatabaseUtils.java @@ -0,0 +1,150 @@ +package io.sentry.jdbc; + +import com.p6spy.engine.common.ConnectionInformation; +import com.p6spy.engine.common.StatementInformation; +import io.sentry.util.StringUtils; +import java.net.URI; +import java.util.Locale; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class DatabaseUtils { + + private static final @NotNull DatabaseDetails EMPTY = new DatabaseDetails(null, null); + + public static DatabaseDetails readFrom( + final @Nullable StatementInformation statementInformation) { + if (statementInformation == null) { + return EMPTY; + } + + final @Nullable ConnectionInformation connectionInformation = + statementInformation.getConnectionInformation(); + if (connectionInformation == null) { + return EMPTY; + } + + return parse(connectionInformation.getUrl()); + } + + public static DatabaseDetails parse(final @Nullable String databaseConnectionUrl) { + if (databaseConnectionUrl == null) { + return EMPTY; + } + try { + final @NotNull String rawUrl = + removeP6SpyPrefix(databaseConnectionUrl.toLowerCase(Locale.ROOT)); + final @NotNull String[] urlParts = rawUrl.split(":", -1); + if (urlParts.length > 1) { + final @NotNull String dbSystem = urlParts[0]; + return parseDbSystemSpecific(dbSystem, urlParts, rawUrl); + } + } catch (Throwable t) { + // ignore + } + + return EMPTY; + } + + private static @NotNull DatabaseDetails parseDbSystemSpecific( + final @NotNull String dbSystem, + final @NotNull String[] urlParts, + final @NotNull String urlString) { + if ("hsqldb".equalsIgnoreCase(dbSystem) + || "h2".equalsIgnoreCase(dbSystem) + || "derby".equalsIgnoreCase(dbSystem) + || "sqlite".equalsIgnoreCase(dbSystem)) { + if (urlString.contains("//")) { + return parseAsUri(dbSystem, StringUtils.removePrefix(urlString, dbSystem + ":")); + } + if (urlParts.length > 2) { + String dbNameAndMaybeMore = urlParts[2]; + return new DatabaseDetails(dbSystem, StringUtils.substringBefore(dbNameAndMaybeMore, ";")); + } + if (urlParts.length > 1) { + String dbNameAndMaybeMore = urlParts[1]; + return new DatabaseDetails(dbSystem, StringUtils.substringBefore(dbNameAndMaybeMore, ";")); + } + } + if ("mariadb".equalsIgnoreCase(dbSystem) + || "mysql".equalsIgnoreCase(dbSystem) + || "postgresql".equalsIgnoreCase(dbSystem) + || "mongo".equalsIgnoreCase(dbSystem)) { + return parseAsUri(dbSystem, urlString); + } + if ("sqlserver".equalsIgnoreCase(dbSystem)) { + try { + String dbProperty = ";databasename="; + final int index = urlString.indexOf(dbProperty); + if (index >= 0) { + final @NotNull String dbNameAndMaybeMore = + urlString.substring(index + dbProperty.length()); + return new DatabaseDetails( + dbSystem, StringUtils.substringBefore(dbNameAndMaybeMore, ";")); + } + } catch (Throwable t) { + // ignore + } + } + if ("oracle".equalsIgnoreCase(dbSystem)) { + String uriPrefix = "oracle:thin:@//"; + final int indexOfUri = urlString.indexOf(uriPrefix); + if (indexOfUri >= 0) { + final @NotNull String uri = + "makethisaprotocol://" + urlString.substring(indexOfUri + uriPrefix.length()); + return parseAsUri(dbSystem, uri); + } + + final int indexOfTnsNames = urlString.indexOf("oracle:thin:@("); + if (indexOfTnsNames >= 0) { + String serviceNamePrefix = "(service_name="; + final int indexOfServiceName = urlString.indexOf(serviceNamePrefix); + if (indexOfServiceName >= 0) { + final int indexOfClosingBrace = urlString.indexOf(")", indexOfServiceName); + final @NotNull String serviceName = + urlString.substring( + indexOfServiceName + serviceNamePrefix.length(), indexOfClosingBrace); + return new DatabaseDetails(dbSystem, serviceName); + } + } + } + + return new DatabaseDetails(dbSystem, null); + } + + private static @NotNull DatabaseDetails parseAsUri( + final @NotNull String dbSystem, final @NotNull String urlString) { + try { + final @NotNull URI url = new URI(urlString); + String path = StringUtils.removePrefix(url.getPath(), "/"); + String pathWithoutProperties = StringUtils.substringBefore(path, ";"); + return new DatabaseDetails(dbSystem, pathWithoutProperties); + } catch (Throwable t) { + System.out.println(t.getMessage()); + // ignore + } + return new DatabaseDetails(dbSystem, null); + } + + private static @NotNull String removeP6SpyPrefix(final @NotNull String url) { + return StringUtils.removePrefix(StringUtils.removePrefix(url, "jdbc:"), "p6spy:"); + } + + public static final class DatabaseDetails { + private final @Nullable String dbSystem; + private final @Nullable String dbName; + + DatabaseDetails(final @Nullable String dbSystem, final @Nullable String dbName) { + this.dbSystem = dbSystem; + this.dbName = dbName; + } + + public @Nullable String getDbSystem() { + return dbSystem; + } + + public @Nullable String getDbName() { + return dbName; + } + } +} 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 0e301cd1c67..0346d2d0b98 100644 --- a/sentry-jdbc/src/main/java/io/sentry/jdbc/SentryJdbcEventListener.java +++ b/sentry-jdbc/src/main/java/io/sentry/jdbc/SentryJdbcEventListener.java @@ -1,5 +1,8 @@ package io.sentry.jdbc; +import static io.sentry.SpanDataConvention.DB_NAME_KEY; +import static io.sentry.SpanDataConvention.DB_SYSTEM_KEY; + import com.jakewharton.nopen.annotation.Open; import com.p6spy.engine.common.StatementInformation; import com.p6spy.engine.event.SimpleJdbcEventListener; @@ -21,6 +24,9 @@ public class SentryJdbcEventListener extends SimpleJdbcEventListener { private final @NotNull IHub hub; private static final @NotNull ThreadLocal CURRENT_SPAN = new ThreadLocal<>(); + private volatile @Nullable DatabaseUtils.DatabaseDetails cachedDatabaseDetails = null; + private final @NotNull Object databaseDetailsLock = new Object(); + public SentryJdbcEventListener(final @NotNull IHub hub) { this.hub = Objects.requireNonNull(hub, "hub is required"); addPackageAndIntegrationInfo(); @@ -46,7 +52,10 @@ public void onAfterAnyExecute( long timeElapsedNanos, final @Nullable SQLException e) { final ISpan span = CURRENT_SPAN.get(); + if (span != null) { + applyDatabaseDetailsToSpan(statementInformation, span); + if (e != null) { span.setThrowable(e); span.setStatus(SpanStatus.INTERNAL_ERROR); @@ -63,4 +72,31 @@ private void addPackageAndIntegrationInfo() { SentryIntegrationPackageStorage.getInstance() .addPackage("maven:io.sentry:sentry-jdbc", BuildConfig.VERSION_NAME); } + + private void applyDatabaseDetailsToSpan( + final @NotNull StatementInformation statementInformation, final @NotNull ISpan span) { + final @NotNull DatabaseUtils.DatabaseDetails databaseDetails = + getOrComputeDatabaseDetails(statementInformation); + + if (databaseDetails.getDbSystem() != null) { + span.setData(DB_SYSTEM_KEY, databaseDetails.getDbSystem()); + } + + if (databaseDetails.getDbName() != null) { + span.setData(DB_NAME_KEY, databaseDetails.getDbName()); + } + } + + private @NotNull DatabaseUtils.DatabaseDetails getOrComputeDatabaseDetails( + final @NotNull StatementInformation statementInformation) { + if (cachedDatabaseDetails == null) { + synchronized (databaseDetailsLock) { + if (cachedDatabaseDetails == null) { + cachedDatabaseDetails = DatabaseUtils.readFrom(statementInformation); + } + } + } + + return cachedDatabaseDetails; + } } diff --git a/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/DatabaseUtilsTest.kt b/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/DatabaseUtilsTest.kt new file mode 100644 index 00000000000..dd6b40b540b --- /dev/null +++ b/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/DatabaseUtilsTest.kt @@ -0,0 +1,199 @@ +package io.sentry.jdbc + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class DatabaseUtilsTest { + + @Test + fun `parses to empty details for null`() { + val details = DatabaseUtils.parse(null) + assertNotNull(details) + assertNull(details.dbSystem) + assertNull(details.dbName) + } + + @Test + fun `detects db system for hsql in-memory`() { + val details = DatabaseUtils.parse("jdbc:p6spy:hsqldb:mem:testdb;a=b") + assertEquals("hsqldb", details.dbSystem) + assertEquals("testdb", details.dbName) + } + + @Test + fun `detects db system for hsql in-memory legacy`() { + val details = DatabaseUtils.parse("jdbc:p6spy:hsqldb:.;a=b") + assertEquals("hsqldb", details.dbSystem) + assertEquals(".", details.dbName) + } + + @Test + fun `detects db system for hsql remote`() { + val details = DatabaseUtils.parse("jdbc:hsqldb:hsql://some-host.com:1234/testdb;a=b") + assertEquals("hsqldb", details.dbSystem) + assertEquals("testdb", details.dbName) + } + + @Test + fun `detects db system for h2 in-memory`() { + val details = DatabaseUtils.parse("jdbc:h2:mem:AZ;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE") + assertEquals("h2", details.dbSystem) + assertEquals("az", details.dbName) + } + + @Test + fun `detects db system for h2 tcp`() { + val details = DatabaseUtils.parse("jdbc:h2:tcp://localhost/~/test") + assertEquals("h2", details.dbSystem) + assertEquals("~/test", details.dbName) + } + + @Test + fun `detects db system for derby`() { + val details = DatabaseUtils.parse("jdbc:derby:sample") + assertEquals("derby", details.dbSystem) + assertEquals("sample", details.dbName) + } + + @Test + fun `detects db system for derby remote`() { + val details = DatabaseUtils.parse("jdbc:derby://some-host.com:1234/sample") + assertEquals("derby", details.dbSystem) + assertEquals("sample", details.dbName) + } + + @Test + fun `detects db system for derby remote no port`() { + val details = DatabaseUtils.parse("jdbc:derby://some-host.com/sample") + assertEquals("derby", details.dbSystem) + assertEquals("sample", details.dbName) + } + + @Test + fun `detects db system for sqlite`() { + val details = DatabaseUtils.parse("jdbc:sqlite:sample.db") + assertEquals("sqlite", details.dbSystem) + assertEquals("sample.db", details.dbName) + } + + @Test + fun `detects db system for sqlite memory`() { + val details = DatabaseUtils.parse("jdbc:sqlite::memory:") + assertEquals("sqlite", details.dbSystem) + assertEquals("memory", details.dbName) + } + + @Test + fun `detects db system for sqlite windows`() { + val details = DatabaseUtils.parse("jdbc:sqlite:C:/sqlite/db/some.db") + assertEquals("sqlite", details.dbSystem) + assertEquals("/sqlite/db/some.db", details.dbName) + } + + @Test + fun `detects db system for mongo`() { + val details = DatabaseUtils.parse("jdbc:mongo://some-server.com:1234/mydb") + assertEquals("mongo", details.dbSystem) + assertEquals("mydb", details.dbName) + } + + @Test + fun `detects db system for mongo no db`() { + val details = DatabaseUtils.parse("jdbc:mongo://some-server.com:1234") + assertEquals("mongo", details.dbSystem) + assertEquals("", details.dbName) + } + + @Test + fun `detects db system for redis`() { + val details = DatabaseUtils.parse("jdbc:redis:Server=127.0.0.1;Port=6379;Password=myPassword;") + assertEquals("redis", details.dbSystem) + assertNull(details.dbName) + } + + @Test + fun `detects db system for dynamodb`() { + val details = DatabaseUtils.parse("jdbc:amazondynamodb:Access Key=xxx;Secret Key=xxx;Domain=amazonaws.com;Region=OREGON;") + assertEquals("amazondynamodb", details.dbSystem) + assertNull(details.dbName) + } + + @Test + fun `detects db system for oracle`() { + val details = DatabaseUtils.parse("jdbc:oracle:thin:@//myoracle.db.server:1521/my_servicename") + assertEquals("oracle", details.dbSystem) + assertEquals("my_servicename", details.dbName) + } + + @Test + fun `detects db system for oracle2`() { + val details = DatabaseUtils.parse("jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=myoracle.db.server)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=my_servicename)))") + assertEquals("oracle", details.dbSystem) + assertEquals("my_servicename", details.dbName) + } + + @Test + fun `detects db system for mariadb`() { + val details = DatabaseUtils.parse("jdbc:mariadb://example.skysql.net:5001/jdbc_demo?useSsl=true&serverSslCert=/path/to/skysql_chain.pem") + assertEquals("mariadb", details.dbSystem) + assertEquals("jdbc_demo", details.dbName) + } + + @Test + fun `detects db system for mariadb no host and port`() { + val details = DatabaseUtils.parse("jdbc:mariadb://") + assertEquals("mariadb", details.dbSystem) + assertNull(details.dbName) + } + + @Test + fun `detects db system for mysql`() { + val details = DatabaseUtils.parse("jdbc:mysql://mysql.db.server:3306/my_database?useSSL=false&serverTimezone=UTC") + assertEquals("mysql", details.dbSystem) + assertEquals("my_database", details.dbName) + } + + @Test + fun `detects db system for mysql no host and port and database`() { + val details = DatabaseUtils.parse("jdbc:mysql://") + assertEquals("mysql", details.dbSystem) + assertNull(details.dbName) + } + + @Test + fun `detects db system for mssql`() { + val details = DatabaseUtils.parse("jdbc:sqlserver://mssql.db.server\\\\mssql_instance;databaseName=my_database") + assertEquals("sqlserver", details.dbSystem) + assertEquals("my_database", details.dbName) + } + + @Test + fun `detects db system for mssql2`() { + val details = DatabaseUtils.parse("jdbc:sqlserver://mssql.db.server\\\\mssql_instance;databaseName=my_database;otherProperty=value") + assertEquals("sqlserver", details.dbSystem) + assertEquals("my_database", details.dbName) + } + + @Test + fun `detects db system for mssql2 no host and port`() { + val details = DatabaseUtils.parse("jdbc:sqlserver://;databaseName=my_database;otherProperty=value") + assertEquals("sqlserver", details.dbSystem) + assertEquals("my_database", details.dbName) + } + + @Test + fun `detects db system for postgres`() { + val details = DatabaseUtils.parse("jdbc:postgresql://postgresql.db.server:5430/my_database?ssl=true&loglevel=2") + assertEquals("postgresql", details.dbSystem) + assertEquals("my_database", details.dbName) + } + + @Test + fun `detects db system for postgres no host and port`() { + val details = DatabaseUtils.parse("jdbc:postgresql:///my_database?ssl=true&loglevel=2") + assertEquals("postgresql", details.dbSystem) + assertEquals("my_database", details.dbName) + } +} 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 962410647e2..78c5d4cf12f 100644 --- a/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/SentryJdbcEventListenerTest.kt +++ b/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/SentryJdbcEventListenerTest.kt @@ -1,13 +1,19 @@ package io.sentry.jdbc +import com.p6spy.engine.common.StatementInformation import com.p6spy.engine.spy.P6DataSource import io.sentry.IHub import io.sentry.SentryOptions import io.sentry.SentryTracer +import io.sentry.SpanDataConvention.DB_NAME_KEY +import io.sentry.SpanDataConvention.DB_SYSTEM_KEY import io.sentry.SpanStatus import io.sentry.TransactionContext +import io.sentry.jdbc.DatabaseUtils.DatabaseDetails import io.sentry.protocol.SdkVersion import org.hsqldb.jdbc.JDBCDataSource +import org.mockito.Mockito +import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import javax.sql.DataSource @@ -131,4 +137,44 @@ class SentryJdbcEventListenerTest { assertNotNull(packageInfo) assert(packageInfo.version == BuildConfig.VERSION_NAME) } + + @Test + fun `sets database details`() { + val sut = fixture.getSut() + + sut.connection.use { + it.prepareStatement("INSERT INTO foo VALUES (1)").executeUpdate() + } + + assertEquals("hsqldb", fixture.tx.children.first().data[DB_SYSTEM_KEY]) + assertEquals("testdb", fixture.tx.children.first().data[DB_NAME_KEY]) + } + + @Test + fun `only parses database details once`() { + Mockito.mockStatic(DatabaseUtils::class.java).use { utils -> + var invocationCount = 0 + utils.`when` { DatabaseUtils.readFrom(any()) } + .thenAnswer { + invocationCount++ + DatabaseDetails("a", "b") + } + val sut = fixture.getSut() + + sut.connection.use { + it.prepareStatement("INSERT INTO foo VALUES (1)").executeUpdate() + it.prepareStatement("INSERT INTO foo VALUES (2)").executeUpdate() + } + + sut.connection.use { + it.prepareStatement("INSERT INTO foo VALUES (3)").executeUpdate() + it.prepareStatement("INSERT INTO foo VALUES (4)").executeUpdate() + } + + assertEquals("a", fixture.tx.children.first().data[DB_SYSTEM_KEY]) + assertEquals("b", fixture.tx.children.first().data[DB_NAME_KEY]) + + assertEquals(1, invocationCount) + } + } } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 978f582bedd..43ca4d5fad5 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2225,6 +2225,8 @@ public final class io/sentry/SpanContext$JsonKeys { public abstract interface class io/sentry/SpanDataConvention { public static final field BLOCKED_MAIN_THREAD_KEY Ljava/lang/String; public static final field CALL_STACK_KEY Ljava/lang/String; + public static final field DB_NAME_KEY Ljava/lang/String; + public static final field DB_SYSTEM_KEY Ljava/lang/String; public static final field HTTP_FRAGMENT_KEY Ljava/lang/String; public static final field HTTP_METHOD_KEY Ljava/lang/String; public static final field HTTP_QUERY_KEY Ljava/lang/String; @@ -4317,7 +4319,9 @@ public final class io/sentry/util/StringUtils { public static fun getStringAfterDot (Ljava/lang/String;)Ljava/lang/String; public static fun join (Ljava/lang/CharSequence;Ljava/lang/Iterable;)Ljava/lang/String; public static fun normalizeUUID (Ljava/lang/String;)Ljava/lang/String; + public static fun removePrefix (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; public static fun removeSurrounding (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; + public static fun substringBefore (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; public static fun toString (Ljava/lang/Object;)Ljava/lang/String; } diff --git a/sentry/src/main/java/io/sentry/SpanDataConvention.java b/sentry/src/main/java/io/sentry/SpanDataConvention.java index 7e8d81ac44c..bffae1206a0 100644 --- a/sentry/src/main/java/io/sentry/SpanDataConvention.java +++ b/sentry/src/main/java/io/sentry/SpanDataConvention.java @@ -6,6 +6,8 @@ public interface SpanDataConvention { // Keys that should respect the span data conventions, as described in // https://develop.sentry.dev/sdk/performance/span-data-conventions/ + String DB_SYSTEM_KEY = "db.system"; + String DB_NAME_KEY = "db.name"; String HTTP_QUERY_KEY = "http.query"; String HTTP_FRAGMENT_KEY = "http.fragment"; String HTTP_METHOD_KEY = "http.method"; diff --git a/sentry/src/main/java/io/sentry/util/StringUtils.java b/sentry/src/main/java/io/sentry/util/StringUtils.java index 2158576619d..ac95f9ad70c 100644 --- a/sentry/src/main/java/io/sentry/util/StringUtils.java +++ b/sentry/src/main/java/io/sentry/util/StringUtils.java @@ -189,4 +189,30 @@ public static String join( return object.toString(); } + + public static @NotNull String removePrefix( + final @Nullable String string, final @NotNull String prefix) { + if (string == null) { + return ""; + } + final int index = string.indexOf(prefix); + if (index == 0) { + return string.substring(prefix.length()); + } else { + return string; + } + } + + public static @NotNull String substringBefore( + final @Nullable String string, final @NotNull String separator) { + if (string == null) { + return ""; + } + final int index = string.indexOf(separator); + if (index >= 0) { + return string.substring(0, index); + } else { + return string; + } + } } diff --git a/sentry/src/test/java/io/sentry/util/StringUtilsTest.kt b/sentry/src/test/java/io/sentry/util/StringUtilsTest.kt index a8c9058c298..d06f3859ae7 100644 --- a/sentry/src/test/java/io/sentry/util/StringUtilsTest.kt +++ b/sentry/src/test/java/io/sentry/util/StringUtilsTest.kt @@ -147,4 +147,44 @@ class StringUtilsTest { val result = StringUtils.join(",", emptyList()) assertEquals("", result) } + + @Test + fun `remove prefix on null returns emtpy string`() { + assertEquals("", StringUtils.removePrefix(null, ":")) + } + + @Test + fun `remove prefix on string equal to prefix returns empty string`() { + assertEquals("", StringUtils.removePrefix(":", ":")) + } + + @Test + fun `remove prefix on string returns string without prefix`() { + assertEquals("abc", StringUtils.removePrefix(":abc", ":")) + } + + @Test + fun `remove prefix on string returns string untouched if prefix is not at start`() { + assertEquals("abc:", StringUtils.removePrefix("abc:", ":")) + } + + @Test + fun `returns only prefix before separator`() { + assertEquals("abc", StringUtils.substringBefore("abc:", ":")) + } + + @Test + fun `returns empty string if string is null substringBefore`() { + assertEquals("", StringUtils.substringBefore(null, ":")) + } + + @Test + fun `returns full string if separator is not in string`() { + assertEquals("abc", StringUtils.substringBefore("abc", ":")) + } + + @Test + fun `returns empty string if separator is at start of string`() { + assertEquals("", StringUtils.substringBefore(":abc", ":")) + } } From 2fb2f6361e8140ed1306828830ff37c66f6d2c0d Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 11 Aug 2023 10:59:55 +0200 Subject: [PATCH 2/7] recurse for certain dbsystems --- .../java/io/sentry/jdbc/DatabaseUtils.java | 6 ++++ .../io/sentry/jdbc/DatabaseUtilsTest.kt | 28 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/sentry-jdbc/src/main/java/io/sentry/jdbc/DatabaseUtils.java b/sentry-jdbc/src/main/java/io/sentry/jdbc/DatabaseUtils.java index 14524c67e2e..a7585a88664 100644 --- a/sentry-jdbc/src/main/java/io/sentry/jdbc/DatabaseUtils.java +++ b/sentry-jdbc/src/main/java/io/sentry/jdbc/DatabaseUtils.java @@ -108,6 +108,12 @@ public static DatabaseDetails parse(final @Nullable String databaseConnectionUrl } } } + if ("datadirect".equalsIgnoreCase(dbSystem) + || "tibcosoftware".equalsIgnoreCase(dbSystem) + || "jtds".equalsIgnoreCase(dbSystem) + || "microsoft".equalsIgnoreCase(dbSystem)) { + return parse(StringUtils.removePrefix(urlString, dbSystem + ":")); + } return new DatabaseDetails(dbSystem, null); } diff --git a/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/DatabaseUtilsTest.kt b/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/DatabaseUtilsTest.kt index dd6b40b540b..ff996f73341 100644 --- a/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/DatabaseUtilsTest.kt +++ b/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/DatabaseUtilsTest.kt @@ -196,4 +196,32 @@ class DatabaseUtilsTest { assertEquals("postgresql", details.dbSystem) assertEquals("my_database", details.dbName) } + + @Test + fun `detects db system for datadirect postgres`() { + val details = DatabaseUtils.parse("jdbc:datadirect:postgresql://postgresql.db.server:5430/my_database?ssl=true&loglevel=2") + assertEquals("postgresql", details.dbSystem) + assertEquals("my_database", details.dbName) + } + + @Test + fun `detects db system for tibcosoftware postgres`() { + val details = DatabaseUtils.parse("jdbc:tibcosoftware:postgresql://postgresql.db.server:5430/my_database?ssl=true&loglevel=2") + assertEquals("postgresql", details.dbSystem) + assertEquals("my_database", details.dbName) + } + + @Test + fun `detects db system for jtds postgres`() { + val details = DatabaseUtils.parse("jdbc:jtds:postgresql://postgresql.db.server:5430/my_database?ssl=true&loglevel=2") + assertEquals("postgresql", details.dbSystem) + assertEquals("my_database", details.dbName) + } + + @Test + fun `detects db system for microsoft sqlserver`() { + val details = DatabaseUtils.parse("jdbc:microsoft:sqlserver://mssql.db.server\\\\mssql_instance;databaseName=my_database") + assertEquals("sqlserver", details.dbSystem) + assertEquals("my_database", details.dbName) + } } From d1c6d56fd78793956337da4175f3fad4cac39931 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 11 Aug 2023 13:00:16 +0200 Subject: [PATCH 3/7] changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f927dd571a..d4e0dd05037 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Features + +- Send `db.system` and `db.name` in span data ([#2894](https://github.com/getsentry/sentry-java/pull/2894)) + ## 6.28.0 ### Features From 50fc3079aae4c27d62920cd5f396b6177b187bce Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 14 Aug 2023 07:46:55 +0200 Subject: [PATCH 4/7] Add test --- .../src/test/kotlin/io/sentry/jdbc/DatabaseUtilsTest.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/DatabaseUtilsTest.kt b/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/DatabaseUtilsTest.kt index ff996f73341..7ff70e49bde 100644 --- a/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/DatabaseUtilsTest.kt +++ b/sentry-jdbc/src/test/kotlin/io/sentry/jdbc/DatabaseUtilsTest.kt @@ -92,6 +92,13 @@ class DatabaseUtilsTest { assertEquals("/sqlite/db/some.db", details.dbName) } + @Test + fun `detects db system for sqlite linux`() { + val details = DatabaseUtils.parse("jdbc:sqlite:/home/sqlite/db/some.db") + assertEquals("sqlite", details.dbSystem) + assertEquals("/home/sqlite/db/some.db", details.dbName) + } + @Test fun `detects db system for mongo`() { val details = DatabaseUtils.parse("jdbc:mongo://some-server.com:1234/mydb") From 5f1b80fbe8f66d47be6b50e8786f858e8708e2a7 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 14 Aug 2023 13:19:34 +0200 Subject: [PATCH 5/7] Add HTTP method to span data --- .../java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt | 4 +++- .../java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt | 4 +++- .../src/main/java/io/sentry/openfeign/SentryFeignClient.java | 5 ++++- .../test/kotlin/io/sentry/openfeign/SentryFeignClientTest.kt | 1 + .../tracing/SentrySpanClientHttpRequestInterceptor.java | 2 ++ .../jakarta/tracing/SentrySpanClientWebRequestFilter.java | 5 ++++- .../tracing/SentrySpanClientHttpRequestInterceptor.java | 2 ++ .../spring/tracing/SentrySpanClientWebRequestFilter.java | 5 ++++- sentry/src/main/java/io/sentry/SpanDataConvention.java | 2 +- 9 files changed, 24 insertions(+), 6 deletions(-) diff --git a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt index 906af7c8c67..2276272fe73 100644 --- a/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt +++ b/sentry-apollo-3/src/main/java/io/sentry/apollo3/SentryApollo3HttpInterceptor.kt @@ -20,6 +20,7 @@ import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryLevel import io.sentry.SentryOptions.DEFAULT_PROPAGATION_TARGETS import io.sentry.SpanDataConvention +import io.sentry.SpanDataConvention.HTTP_METHOD_KEY import io.sentry.SpanStatus import io.sentry.TypeCheckHint.APOLLO_REQUEST import io.sentry.TypeCheckHint.APOLLO_RESPONSE @@ -34,6 +35,7 @@ import io.sentry.util.UrlUtils import io.sentry.vendor.Base64 import okio.Buffer import org.jetbrains.annotations.ApiStatus +import java.util.Locale private const val TRACE_ORIGIN = "auto.graphql.apollo3" @@ -171,7 +173,7 @@ class SentryApollo3HttpInterceptor @JvmOverloads constructor( variables?.let { setData("variables", it) } - setData("http.method", method) + setData(HTTP_METHOD_KEY, method.toUpperCase(Locale.ROOT)) } } diff --git a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt index d197eeae4a0..5b9ab998863 100644 --- a/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt +++ b/sentry-apollo-3/src/test/java/io/sentry/apollo3/SentryApollo3InterceptorTest.kt @@ -18,6 +18,7 @@ import io.sentry.SentryOptions.DEFAULT_PROPAGATION_TARGETS import io.sentry.SentryTraceHeader import io.sentry.SentryTracer import io.sentry.SpanDataConvention +import io.sentry.SpanDataConvention.HTTP_METHOD_KEY import io.sentry.SpanStatus import io.sentry.TraceContext import io.sentry.TracesSamplingDecision @@ -154,6 +155,7 @@ class SentryApollo3InterceptorTest { verify(fixture.hub).captureTransaction( check { assertTransactionDetails(it, httpStatusCode = 404, contentLength = null) + assertEquals("POST", it.spans.first().data?.get(SpanDataConvention.HTTP_METHOD_KEY)) assertEquals(404, it.spans.first().data?.get(SpanDataConvention.HTTP_STATUS_CODE_KEY)) assertEquals(SpanStatus.NOT_FOUND, it.spans.first().status) }, @@ -314,7 +316,7 @@ class SentryApollo3InterceptorTest { assertTrue { httpClientSpan.description?.startsWith("Post LaunchDetails") == true } assertNotNull(httpClientSpan.data) { assertNotNull(it["operationId"]) - assertEquals("Post", it["http.method"]) + assertEquals("POST", it[HTTP_METHOD_KEY]) httpStatusCode?.let { code -> assertEquals(code, it[SpanDataConvention.HTTP_STATUS_CODE_KEY]) } diff --git a/sentry-openfeign/src/main/java/io/sentry/openfeign/SentryFeignClient.java b/sentry-openfeign/src/main/java/io/sentry/openfeign/SentryFeignClient.java index ca93bd88325..cb8aa3d9e08 100644 --- a/sentry-openfeign/src/main/java/io/sentry/openfeign/SentryFeignClient.java +++ b/sentry-openfeign/src/main/java/io/sentry/openfeign/SentryFeignClient.java @@ -21,6 +21,7 @@ import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.Locale; import java.util.Map; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -56,7 +57,9 @@ public Response execute(final @NotNull Request request, final @NotNull Request.O ISpan span = activeSpan.startChild("http.client"); span.getSpanContext().setOrigin(TRACE_ORIGIN); final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(request.url()); - span.setDescription(request.httpMethod().name() + " " + urlDetails.getUrlOrFallback()); + final @NotNull String method = request.httpMethod().name(); + span.setDescription(method + " " + urlDetails.getUrlOrFallback()); + span.setData(SpanDataConvention.HTTP_METHOD_KEY, method.toUpperCase(Locale.ROOT)); urlDetails.applyToSpan(span); final @NotNull Request modifiedRequest = maybeAddTracingHeaders(request, span); diff --git a/sentry-openfeign/src/test/kotlin/io/sentry/openfeign/SentryFeignClientTest.kt b/sentry-openfeign/src/test/kotlin/io/sentry/openfeign/SentryFeignClientTest.kt index 07e59105234..65e56ab02bc 100644 --- a/sentry-openfeign/src/test/kotlin/io/sentry/openfeign/SentryFeignClientTest.kt +++ b/sentry-openfeign/src/test/kotlin/io/sentry/openfeign/SentryFeignClientTest.kt @@ -167,6 +167,7 @@ class SentryFeignClientTest { assertEquals("http.client", httpClientSpan.operation) assertEquals("GET ${fixture.server.url("/status/200")}", httpClientSpan.description) assertEquals(201, httpClientSpan.data[SpanDataConvention.HTTP_STATUS_CODE_KEY]) + assertEquals("GET", httpClientSpan.data[SpanDataConvention.HTTP_METHOD_KEY]) assertEquals(SpanStatus.OK, httpClientSpan.status) assertEquals("auto.http.openfeign", httpClientSpan.spanContext.origin) assertTrue(httpClientSpan.isFinished) diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptor.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptor.java index 1190d4c9956..d190a0427d3 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptor.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptor.java @@ -16,6 +16,7 @@ import io.sentry.util.TracingUtils; import io.sentry.util.UrlUtils; import java.io.IOException; +import java.util.Locale; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.springframework.http.HttpRequest; @@ -53,6 +54,7 @@ public SentrySpanClientHttpRequestInterceptor(final @NotNull IHub hub) { request.getMethod() != null ? request.getMethod().name() : "unknown"; final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(request.getURI().toString()); span.setDescription(methodName + " " + urlDetails.getUrlOrFallback()); + span.setData(SpanDataConvention.HTTP_METHOD_KEY, methodName.toUpperCase(Locale.ROOT)); urlDetails.applyToSpan(span); maybeAddTracingHeaders(request, span); diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientWebRequestFilter.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientWebRequestFilter.java index 4e90a487590..4ac511721ee 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientWebRequestFilter.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientWebRequestFilter.java @@ -13,6 +13,7 @@ import io.sentry.SpanStatus; import io.sentry.util.Objects; import io.sentry.util.TracingUtils; +import java.util.Locale; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.springframework.web.reactive.function.client.ClientRequest; @@ -42,7 +43,9 @@ public SentrySpanClientWebRequestFilter(final @NotNull IHub hub) { final ISpan span = activeSpan.startChild("http.client"); span.getSpanContext().setOrigin(TRACE_ORIGIN); - span.setDescription(request.method().name() + " " + request.url()); + final @NotNull String method = request.method().name(); + span.setDescription(method + " " + request.url()); + span.setData(SpanDataConvention.HTTP_METHOD_KEY, method.toUpperCase(Locale.ROOT)); final @NotNull ClientRequest modifiedRequest = maybeAddTracingHeaders(request, span); diff --git a/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanClientHttpRequestInterceptor.java b/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanClientHttpRequestInterceptor.java index da3dffc3cbd..4067c69c8c2 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanClientHttpRequestInterceptor.java +++ b/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanClientHttpRequestInterceptor.java @@ -16,6 +16,7 @@ import io.sentry.util.TracingUtils; import io.sentry.util.UrlUtils; import java.io.IOException; +import java.util.Locale; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.springframework.http.HttpRequest; @@ -54,6 +55,7 @@ public SentrySpanClientHttpRequestInterceptor(final @NotNull IHub hub) { final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(request.getURI().toString()); urlDetails.applyToSpan(span); span.setDescription(methodName + " " + urlDetails.getUrlOrFallback()); + span.setData(SpanDataConvention.HTTP_METHOD_KEY, methodName.toUpperCase(Locale.ROOT)); maybeAddTracingHeaders(request, span); diff --git a/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanClientWebRequestFilter.java b/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanClientWebRequestFilter.java index 1fce033a9dc..00ad91bbb03 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanClientWebRequestFilter.java +++ b/sentry-spring/src/main/java/io/sentry/spring/tracing/SentrySpanClientWebRequestFilter.java @@ -14,6 +14,7 @@ import io.sentry.util.Objects; import io.sentry.util.TracingUtils; import io.sentry.util.UrlUtils; +import java.util.Locale; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.springframework.web.reactive.function.client.ClientRequest; @@ -43,7 +44,9 @@ public SentrySpanClientWebRequestFilter(final @NotNull IHub hub) { final ISpan span = activeSpan.startChild("http.client"); span.getSpanContext().setOrigin(TRACE_ORIGIN); final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(request.url().toString()); - span.setDescription(request.method().name() + " " + urlDetails.getUrlOrFallback()); + final @NotNull String method = request.method().name(); + span.setDescription(method + " " + urlDetails.getUrlOrFallback()); + span.setData(SpanDataConvention.HTTP_METHOD_KEY, method.toUpperCase(Locale.ROOT)); urlDetails.applyToSpan(span); final ClientRequest clientRequestWithSentryTraceHeader = maybeAddHeaders(request, span); diff --git a/sentry/src/main/java/io/sentry/SpanDataConvention.java b/sentry/src/main/java/io/sentry/SpanDataConvention.java index bffae1206a0..e08736d646d 100644 --- a/sentry/src/main/java/io/sentry/SpanDataConvention.java +++ b/sentry/src/main/java/io/sentry/SpanDataConvention.java @@ -10,7 +10,7 @@ public interface SpanDataConvention { String DB_NAME_KEY = "db.name"; String HTTP_QUERY_KEY = "http.query"; String HTTP_FRAGMENT_KEY = "http.fragment"; - String HTTP_METHOD_KEY = "http.method"; + String HTTP_METHOD_KEY = "http.request.method"; String HTTP_STATUS_CODE_KEY = "http.response.status_code"; String HTTP_RESPONSE_CONTENT_LENGTH_KEY = "http.response_content_length"; String BLOCKED_MAIN_THREAD_KEY = "blocked_main_thread"; From 615568f949e0b52297ae43314a82a5f69a7498f4 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 14 Aug 2023 13:36:39 +0200 Subject: [PATCH 6/7] Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4e0dd05037..a57d504fa0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - Send `db.system` and `db.name` in span data ([#2894](https://github.com/getsentry/sentry-java/pull/2894)) +- Send `http.request.method` in span data ([#2896](https://github.com/getsentry/sentry-java/pull/2896)) ## 6.28.0 From 07ed4d27217b5f7080914d645e0c58d513605e57 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 16 Aug 2023 15:26:10 +0200 Subject: [PATCH 7/7] Apollo2 + okhttp --- .../java/io/sentry/android/okhttp/SentryOkHttpEvent.kt | 3 ++- .../main/java/io/sentry/apollo/SentryApolloInterceptor.kt | 7 +++++++ .../java/io/sentry/apollo/SentryApolloInterceptorTest.kt | 3 +++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEvent.kt b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEvent.kt index e96829e12ea..2fc718e822a 100644 --- a/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEvent.kt +++ b/sentry-android-okhttp/src/main/java/io/sentry/android/okhttp/SentryOkHttpEvent.kt @@ -15,6 +15,7 @@ import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.RESPONSE_BOD import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.RESPONSE_HEADERS_EVENT import io.sentry.android.okhttp.SentryOkHttpEventListener.Companion.SECURE_CONNECT_EVENT import io.sentry.util.UrlUtils +import java.util.Locale import okhttp3.Request import okhttp3.Response import java.util.concurrent.ConcurrentHashMap @@ -50,7 +51,7 @@ internal class SentryOkHttpEvent(private val hub: IHub, private val request: Req callRootSpan?.setData("url", url) callRootSpan?.setData("host", host) callRootSpan?.setData("path", encodedPath) - callRootSpan?.setData(SpanDataConvention.HTTP_METHOD_KEY, method) + callRootSpan?.setData(SpanDataConvention.HTTP_METHOD_KEY, method.toUpperCase(Locale.ROOT)) } /** diff --git a/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt b/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt index bc8e37d83ef..0a11e1b7626 100644 --- a/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt +++ b/sentry-apollo/src/main/java/io/sentry/apollo/SentryApolloInterceptor.kt @@ -26,6 +26,7 @@ import io.sentry.SpanStatus import io.sentry.TypeCheckHint.APOLLO_REQUEST import io.sentry.TypeCheckHint.APOLLO_RESPONSE import io.sentry.util.TracingUtils +import java.util.Locale import java.util.concurrent.Executor private const val TRACE_ORIGIN = "auto.graphql.apollo" @@ -72,6 +73,12 @@ class SentryApolloInterceptor( } else { span.status = SpanStatus.UNKNOWN } + response.httpResponse.map { it.request().method() }.orNull()?.let { + span.setData( + SpanDataConvention.HTTP_METHOD_KEY, + it.toUpperCase(Locale.ROOT) + ) + } finish(span, requestWithHeader, response) callBack.onResponse(response) diff --git a/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt b/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt index 67e19112ca3..f45c310601b 100644 --- a/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt +++ b/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt @@ -100,6 +100,7 @@ class SentryApolloInterceptorTest { check { assertTransactionDetails(it) assertEquals(SpanStatus.OK, it.spans.first().status) + assertEquals("POST", it.spans.first().data?.get(SpanDataConvention.HTTP_METHOD_KEY)) }, anyOrNull(), anyOrNull(), @@ -116,6 +117,8 @@ class SentryApolloInterceptorTest { assertTransactionDetails(it) assertEquals(SpanStatus.PERMISSION_DENIED, it.spans.first().status) assertEquals(403, it.spans.first().data?.get(SpanDataConvention.HTTP_STATUS_CODE_KEY)) + // we do not have access to the request and method in case of an error + assertNull(it.spans.first().data?.get(SpanDataConvention.HTTP_METHOD_KEY)) }, anyOrNull(), anyOrNull(),