diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c97816359..9632a4a0bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- Support collections and arrays in log attribute type inference ([#5124](https://github.com/getsentry/sentry-java/pull/5124)) - Add scope-level attributes API ([#5118](https://github.com/getsentry/sentry-java/pull/5118)) - Automatically include scope attributes in logs and metrics ([#5120](https://github.com/getsentry/sentry-java/pull/5120)) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 9a7d360fce..f6f85729f1 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2813,6 +2813,7 @@ public final class io/sentry/SentryAppStartProfilingOptions$JsonKeys { } public final class io/sentry/SentryAttribute { + public static fun arrayAttribute (Ljava/lang/String;Ljava/util/Collection;)Lio/sentry/SentryAttribute; public static fun booleanAttribute (Ljava/lang/String;Ljava/lang/Boolean;)Lio/sentry/SentryAttribute; public static fun doubleAttribute (Ljava/lang/String;Ljava/lang/Double;)Lio/sentry/SentryAttribute; public fun getName ()Ljava/lang/String; @@ -2824,6 +2825,7 @@ public final class io/sentry/SentryAttribute { } public final class io/sentry/SentryAttributeType : java/lang/Enum { + public static final field ARRAY Lio/sentry/SentryAttributeType; public static final field BOOLEAN Lio/sentry/SentryAttributeType; public static final field DOUBLE Lio/sentry/SentryAttributeType; public static final field INTEGER Lio/sentry/SentryAttributeType; diff --git a/sentry/src/main/java/io/sentry/SentryAttribute.java b/sentry/src/main/java/io/sentry/SentryAttribute.java index 4bcef14ee8..5064eeedf6 100644 --- a/sentry/src/main/java/io/sentry/SentryAttribute.java +++ b/sentry/src/main/java/io/sentry/SentryAttribute.java @@ -1,5 +1,6 @@ package io.sentry; +import java.util.Collection; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -54,4 +55,9 @@ private SentryAttribute( final @NotNull String name, final @Nullable String value) { return new SentryAttribute(name, SentryAttributeType.STRING, value); } + + public static @NotNull SentryAttribute arrayAttribute( + final @NotNull String name, final @Nullable Collection value) { + return new SentryAttribute(name, SentryAttributeType.ARRAY, value); + } } diff --git a/sentry/src/main/java/io/sentry/SentryAttributeType.java b/sentry/src/main/java/io/sentry/SentryAttributeType.java index 86325c248a..b664817963 100644 --- a/sentry/src/main/java/io/sentry/SentryAttributeType.java +++ b/sentry/src/main/java/io/sentry/SentryAttributeType.java @@ -1,6 +1,7 @@ package io.sentry; import java.math.BigInteger; +import java.util.Collection; import java.util.Locale; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -11,7 +12,8 @@ public enum SentryAttributeType { STRING, BOOLEAN, INTEGER, - DOUBLE; + DOUBLE, + ARRAY; public @NotNull String apiName() { return name().toLowerCase(Locale.ROOT); @@ -33,6 +35,9 @@ public enum SentryAttributeType { if (value instanceof Number) { return DOUBLE; } + if (value instanceof Collection || (value != null && value.getClass().isArray())) { + return ARRAY; + } return STRING; } } diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index 73a14b38e7..46dd5bf5f5 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -2726,10 +2726,12 @@ class ScopesTest { SentryAttribute.booleanAttribute("boolattr", true), SentryAttribute.integerAttribute("intattr", 17), SentryAttribute.doubleAttribute("doubleattr", 3.8), + SentryAttribute.arrayAttribute("arrayattr", listOf("a", "b")), SentryAttribute.named("namedstrattr", "namedstrval"), SentryAttribute.named("namedboolattr", false), SentryAttribute.named("namedintattr", 18), SentryAttribute.named("nameddoubleattr", 4.9), + SentryAttribute.named("namedarrayattr", listOf("x", "y")), ) ), "log message", @@ -2758,6 +2760,10 @@ class ScopesTest { assertEquals(3.8, doubleattr.value) assertEquals("double", doubleattr.type) + val arrayattr = it.attributes?.get("arrayattr")!! + assertEquals(listOf("a", "b"), arrayattr.value) + assertEquals("array", arrayattr.type) + val namedstrattr = it.attributes?.get("namedstrattr")!! assertEquals("namedstrval", namedstrattr.value) assertEquals("string", namedstrattr.type) @@ -2773,6 +2779,10 @@ class ScopesTest { val nameddoubleattr = it.attributes?.get("nameddoubleattr")!! assertEquals(4.9, nameddoubleattr.value) assertEquals("double", nameddoubleattr.type) + + val namedarrayattr = it.attributes?.get("namedarrayattr")!! + assertEquals(listOf("x", "y"), namedarrayattr.value) + assertEquals("array", namedarrayattr.type) }, anyOrNull(), ) @@ -3460,10 +3470,12 @@ class ScopesTest { SentryAttribute.booleanAttribute("boolattr", true), SentryAttribute.integerAttribute("intattr", 17), SentryAttribute.doubleAttribute("doubleattr", 3.8), + SentryAttribute.arrayAttribute("arrayattr", listOf("a", "b")), SentryAttribute.named("namedstrattr", "namedstrval"), SentryAttribute.named("namedboolattr", false), SentryAttribute.named("namedintattr", 18), SentryAttribute.named("nameddoubleattr", 4.9), + SentryAttribute.named("namedarrayattr", listOf("x", "y")), ) ), ) @@ -3492,6 +3504,10 @@ class ScopesTest { assertEquals(3.8, doubleattr.value) assertEquals("double", doubleattr.type) + val arrayattr = it.attributes?.get("arrayattr")!! + assertEquals(listOf("a", "b"), arrayattr.value) + assertEquals("array", arrayattr.type) + val namedstrattr = it.attributes?.get("namedstrattr")!! assertEquals("namedstrval", namedstrattr.value) assertEquals("string", namedstrattr.type) @@ -3507,6 +3523,10 @@ class ScopesTest { val nameddoubleattr = it.attributes?.get("nameddoubleattr")!! assertEquals(4.9, nameddoubleattr.value) assertEquals("double", nameddoubleattr.type) + + val namedarrayattr = it.attributes?.get("namedarrayattr")!! + assertEquals(listOf("x", "y"), namedarrayattr.value) + assertEquals("array", namedarrayattr.type) }, anyOrNull(), anyOrNull(), @@ -3629,10 +3649,12 @@ class ScopesTest { SentryAttribute.booleanAttribute("boolattr", true), SentryAttribute.integerAttribute("intattr", 17), SentryAttribute.doubleAttribute("doubleattr", 3.8), + SentryAttribute.arrayAttribute("arrayattr", listOf("a", "b")), SentryAttribute.named("namedstrattr", "namedstrval"), SentryAttribute.named("namedboolattr", false), SentryAttribute.named("namedintattr", 18), SentryAttribute.named("nameddoubleattr", 4.9), + SentryAttribute.named("namedarrayattr", listOf("x", "y")), ) ), ) @@ -3661,6 +3683,10 @@ class ScopesTest { assertEquals(3.8, doubleattr.value) assertEquals("double", doubleattr.type) + val arrayattr = it.attributes?.get("arrayattr")!! + assertEquals(listOf("a", "b"), arrayattr.value) + assertEquals("array", arrayattr.type) + val namedstrattr = it.attributes?.get("namedstrattr")!! assertEquals("namedstrval", namedstrattr.value) assertEquals("string", namedstrattr.type) @@ -3676,6 +3702,10 @@ class ScopesTest { val nameddoubleattr = it.attributes?.get("nameddoubleattr")!! assertEquals(4.9, nameddoubleattr.value) assertEquals("double", nameddoubleattr.type) + + val namedarrayattr = it.attributes?.get("namedarrayattr")!! + assertEquals(listOf("x", "y"), namedarrayattr.value) + assertEquals("array", namedarrayattr.type) }, anyOrNull(), anyOrNull(), @@ -3798,10 +3828,12 @@ class ScopesTest { SentryAttribute.booleanAttribute("boolattr", true), SentryAttribute.integerAttribute("intattr", 17), SentryAttribute.doubleAttribute("doubleattr", 3.8), + SentryAttribute.arrayAttribute("arrayattr", listOf("a", "b")), SentryAttribute.named("namedstrattr", "namedstrval"), SentryAttribute.named("namedboolattr", false), SentryAttribute.named("namedintattr", 18), SentryAttribute.named("nameddoubleattr", 4.9), + SentryAttribute.named("namedarrayattr", listOf("x", "y")), ) ), ) @@ -3830,6 +3862,10 @@ class ScopesTest { assertEquals(3.8, doubleattr.value) assertEquals("double", doubleattr.type) + val arrayattr = it.attributes?.get("arrayattr")!! + assertEquals(listOf("a", "b"), arrayattr.value) + assertEquals("array", arrayattr.type) + val namedstrattr = it.attributes?.get("namedstrattr")!! assertEquals("namedstrval", namedstrattr.value) assertEquals("string", namedstrattr.type) @@ -3845,6 +3881,10 @@ class ScopesTest { val nameddoubleattr = it.attributes?.get("nameddoubleattr")!! assertEquals(4.9, nameddoubleattr.value) assertEquals("double", nameddoubleattr.type) + + val namedarrayattr = it.attributes?.get("namedarrayattr")!! + assertEquals(listOf("x", "y"), namedarrayattr.value) + assertEquals("array", namedarrayattr.type) }, anyOrNull(), anyOrNull(), diff --git a/sentry/src/test/java/io/sentry/SentryAttributeTypeTest.kt b/sentry/src/test/java/io/sentry/SentryAttributeTypeTest.kt index f1e1cf169a..9516d89b7e 100644 --- a/sentry/src/test/java/io/sentry/SentryAttributeTypeTest.kt +++ b/sentry/src/test/java/io/sentry/SentryAttributeTypeTest.kt @@ -77,4 +77,39 @@ class SentryAttributeTypeTest { fun `inferFrom returns STRING for null`() { assertEquals(SentryAttributeType.STRING, SentryAttributeType.inferFrom(null)) } + + @Test + fun `inferFrom returns ARRAY for List of Strings`() { + assertEquals(SentryAttributeType.ARRAY, SentryAttributeType.inferFrom(listOf("a", "b"))) + } + + @Test + fun `inferFrom returns ARRAY for List of Integers`() { + assertEquals(SentryAttributeType.ARRAY, SentryAttributeType.inferFrom(listOf(1, 2, 3))) + } + + @Test + fun `inferFrom returns ARRAY for Set of Booleans`() { + assertEquals(SentryAttributeType.ARRAY, SentryAttributeType.inferFrom(setOf(true, false))) + } + + @Test + fun `inferFrom returns ARRAY for String array`() { + assertEquals(SentryAttributeType.ARRAY, SentryAttributeType.inferFrom(arrayOf("a", "b"))) + } + + @Test + fun `inferFrom returns ARRAY for int array`() { + assertEquals(SentryAttributeType.ARRAY, SentryAttributeType.inferFrom(intArrayOf(1, 2))) + } + + @Test + fun `inferFrom returns ARRAY for empty list`() { + assertEquals(SentryAttributeType.ARRAY, SentryAttributeType.inferFrom(emptyList())) + } + + @Test + fun `inferFrom returns ARRAY for mixed-type list`() { + assertEquals(SentryAttributeType.ARRAY, SentryAttributeType.inferFrom(listOf("a", 1, true))) + } } diff --git a/sentry/src/test/java/io/sentry/protocol/SentryLogsSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/SentryLogsSerializationTest.kt index c65a3cca70..ade038a408 100644 --- a/sentry/src/test/java/io/sentry/protocol/SentryLogsSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/SentryLogsSerializationTest.kt @@ -38,6 +38,7 @@ class SentryLogsSerializationTest { "sentry.sdk.name" to SentryLogEventAttributeValue("string", "sentry.java.spring-boot.jakarta"), "sentry.environment" to SentryLogEventAttributeValue("string", "production"), + "custom.array" to SentryLogEventAttributeValue("array", listOf("a", "b")), "sentry.sdk.version" to SentryLogEventAttributeValue("string", "8.11.1"), "sentry.trace.parent_span_id" to SentryLogEventAttributeValue("string", "f28b86350e534671"), diff --git a/sentry/src/test/resources/json/sentry_logs.json b/sentry/src/test/resources/json/sentry_logs.json index e78f5af1b0..1674a4f576 100644 --- a/sentry/src/test/resources/json/sentry_logs.json +++ b/sentry/src/test/resources/json/sentry_logs.json @@ -20,6 +20,11 @@ "type": "string", "value": "production" }, + "custom.array": + { + "type": "array", + "value": ["a", "b"] + }, "sentry.sdk.version": { "type": "string",