From 858da6c8327b9087847d046e69b391eca19151c4 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 26 Feb 2026 14:41:55 +0100 Subject: [PATCH 1/3] feat: Support collections and arrays in log attribute type inference Co-Authored-By: Claude Opus 4.6 --- sentry/api/sentry.api | 2 ++ .../main/java/io/sentry/SentryAttribute.java | 6 ++++ .../java/io/sentry/SentryAttributeType.java | 7 +++- .../java/io/sentry/SentryAttributeTypeTest.kt | 35 +++++++++++++++++++ .../protocol/SentryLogsSerializationTest.kt | 1 + .../src/test/resources/json/sentry_logs.json | 5 +++ 6 files changed, 55 insertions(+), 1 deletion(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 9a7d360fce4..f6f85729f10 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 4bcef14ee8c..5064eeedf67 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 86325c248af..b6648179631 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/SentryAttributeTypeTest.kt b/sentry/src/test/java/io/sentry/SentryAttributeTypeTest.kt index f1e1cf169a1..9516d89b7e0 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 c65a3cca709..ade038a408b 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 e78f5af1b09..1674a4f5764 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", From ff23f6e42c1958903a68304b66fc54ab2164fea6 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 26 Feb 2026 14:45:04 +0100 Subject: [PATCH 2/3] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c978163593..9632a4a0bb4 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)) From 8e61d4dfb0c7c8147041652572b68636cd06db02 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 27 Feb 2026 13:44:26 +0100 Subject: [PATCH 3/3] test: Add coverage for arrayAttribute factory method Add arrayAttribute and named array attribute usage to the four attribute tests in ScopesTest (log, count metric, distribution metric, gauge metric) to verify the factory method works end-to-end. Co-Authored-By: Claude --- sentry/src/test/java/io/sentry/ScopesTest.kt | 40 ++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index 73a14b38e71..46dd5bf5f58 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(),