From ff555aa4a17b9d993d78f6591c9c63383ba3c1ec Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 26 Feb 2026 10:49:39 +0100 Subject: [PATCH 01/20] feat(core): Add scope-level attributes API Add setAttribute, setAttributes, removeAttribute, and getAttributes to IScope/IScopes/Sentry so users can set attributes on the scope that are automatically included in logs and metrics events. Also refactor type inference logic into SentryAttributeType.inferFrom and add SentryLogEventAttributeValue.fromAttribute factory method, removing duplicate getType helpers from LoggerApi and MetricsApi. Co-Authored-By: Claude --- sentry/api/sentry.api | 54 +++++++++ .../java/io/sentry/CombinedScopeView.java | 30 +++++ .../src/main/java/io/sentry/HubAdapter.java | 20 ++++ .../main/java/io/sentry/HubScopesWrapper.java | 20 ++++ sentry/src/main/java/io/sentry/IScope.java | 38 +++++++ sentry/src/main/java/io/sentry/IScopes.java | 29 +++++ sentry/src/main/java/io/sentry/NoOpHub.java | 12 ++ sentry/src/main/java/io/sentry/NoOpScope.java | 18 +++ .../src/main/java/io/sentry/NoOpScopes.java | 12 ++ sentry/src/main/java/io/sentry/Scope.java | 70 ++++++++++++ sentry/src/main/java/io/sentry/Scopes.java | 20 ++++ .../main/java/io/sentry/ScopesAdapter.java | 20 ++++ sentry/src/main/java/io/sentry/Sentry.java | 37 +++++++ .../java/io/sentry/SentryAttributeType.java | 14 +++ .../sentry/SentryLogEventAttributeValue.java | 15 +++ .../main/java/io/sentry/logger/LoggerApi.java | 29 ++--- .../java/io/sentry/metrics/MetricsApi.java | 27 ++--- .../java/io/sentry/CombinedScopeViewTest.kt | 67 +++++++++++ sentry/src/test/java/io/sentry/ScopeTest.kt | 104 ++++++++++++++++++ 19 files changed, 601 insertions(+), 35 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index a043f8fe85c..9a7d360fce4 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -276,6 +276,7 @@ public final class io/sentry/CombinedScopeView : io/sentry/IScope { public synthetic fun clone ()Ljava/lang/Object; public fun endSession ()Lio/sentry/Session; public fun getAttachments ()Ljava/util/List; + public fun getAttributes ()Ljava/util/Map; public fun getBreadcrumbs ()Ljava/util/Queue; public fun getClient ()Lio/sentry/ISentryClient; public fun getContexts ()Lio/sentry/protocol/Contexts; @@ -298,11 +299,15 @@ public final class io/sentry/CombinedScopeView : io/sentry/IScope { public fun getTransaction ()Lio/sentry/ITransaction; public fun getTransactionName ()Ljava/lang/String; public fun getUser ()Lio/sentry/protocol/User; + public fun removeAttribute (Ljava/lang/String;)V public fun removeContexts (Ljava/lang/String;)V public fun removeExtra (Ljava/lang/String;)V public fun removeTag (Ljava/lang/String;)V public fun replaceOptions (Lio/sentry/SentryOptions;)V public fun setActiveSpan (Lio/sentry/ISpan;)V + public fun setAttribute (Lio/sentry/SentryAttribute;)V + public fun setAttribute (Ljava/lang/String;Ljava/lang/Object;)V + public fun setAttributes (Lio/sentry/SentryAttributes;)V public fun setContexts (Ljava/lang/String;Ljava/lang/Boolean;)V public fun setContexts (Ljava/lang/String;Ljava/lang/Character;)V public fun setContexts (Ljava/lang/String;Ljava/lang/Number;)V @@ -670,10 +675,14 @@ public final class io/sentry/HubAdapter : io/sentry/IHub { public fun popScope ()V public fun pushIsolationScope ()Lio/sentry/ISentryLifecycleToken; public fun pushScope ()Lio/sentry/ISentryLifecycleToken; + public fun removeAttribute (Ljava/lang/String;)V public fun removeExtra (Ljava/lang/String;)V public fun removeTag (Ljava/lang/String;)V public fun reportFullyDisplayed ()V public fun setActiveSpan (Lio/sentry/ISpan;)V + public fun setAttribute (Lio/sentry/SentryAttribute;)V + public fun setAttribute (Ljava/lang/String;Ljava/lang/Object;)V + public fun setAttributes (Lio/sentry/SentryAttributes;)V public fun setExtra (Ljava/lang/String;Ljava/lang/String;)V public fun setFingerprint (Ljava/util/List;)V public fun setLevel (Lio/sentry/SentryLevel;)V @@ -742,10 +751,14 @@ public final class io/sentry/HubScopesWrapper : io/sentry/IHub { public fun popScope ()V public fun pushIsolationScope ()Lio/sentry/ISentryLifecycleToken; public fun pushScope ()Lio/sentry/ISentryLifecycleToken; + public fun removeAttribute (Ljava/lang/String;)V public fun removeExtra (Ljava/lang/String;)V public fun removeTag (Ljava/lang/String;)V public fun reportFullyDisplayed ()V public fun setActiveSpan (Lio/sentry/ISpan;)V + public fun setAttribute (Lio/sentry/SentryAttribute;)V + public fun setAttribute (Ljava/lang/String;Ljava/lang/Object;)V + public fun setAttributes (Lio/sentry/SentryAttributes;)V public fun setExtra (Ljava/lang/String;Ljava/lang/String;)V public fun setFingerprint (Ljava/util/List;)V public fun setLevel (Lio/sentry/SentryLevel;)V @@ -865,6 +878,7 @@ public abstract interface class io/sentry/IScope { public abstract fun clone ()Lio/sentry/IScope; public abstract fun endSession ()Lio/sentry/Session; public abstract fun getAttachments ()Ljava/util/List; + public abstract fun getAttributes ()Ljava/util/Map; public abstract fun getBreadcrumbs ()Ljava/util/Queue; public abstract fun getClient ()Lio/sentry/ISentryClient; public abstract fun getContexts ()Lio/sentry/protocol/Contexts; @@ -887,11 +901,15 @@ public abstract interface class io/sentry/IScope { public abstract fun getTransaction ()Lio/sentry/ITransaction; public abstract fun getTransactionName ()Ljava/lang/String; public abstract fun getUser ()Lio/sentry/protocol/User; + public abstract fun removeAttribute (Ljava/lang/String;)V public abstract fun removeContexts (Ljava/lang/String;)V public abstract fun removeExtra (Ljava/lang/String;)V public abstract fun removeTag (Ljava/lang/String;)V public abstract fun replaceOptions (Lio/sentry/SentryOptions;)V public abstract fun setActiveSpan (Lio/sentry/ISpan;)V + public abstract fun setAttribute (Lio/sentry/SentryAttribute;)V + public abstract fun setAttribute (Ljava/lang/String;Ljava/lang/Object;)V + public abstract fun setAttributes (Lio/sentry/SentryAttributes;)V public abstract fun setContexts (Ljava/lang/String;Ljava/lang/Boolean;)V public abstract fun setContexts (Ljava/lang/String;Ljava/lang/Character;)V public abstract fun setContexts (Ljava/lang/String;Ljava/lang/Number;)V @@ -1003,10 +1021,14 @@ public abstract interface class io/sentry/IScopes { public abstract fun popScope ()V public abstract fun pushIsolationScope ()Lio/sentry/ISentryLifecycleToken; public abstract fun pushScope ()Lio/sentry/ISentryLifecycleToken; + public abstract fun removeAttribute (Ljava/lang/String;)V public abstract fun removeExtra (Ljava/lang/String;)V public abstract fun removeTag (Ljava/lang/String;)V public abstract fun reportFullyDisplayed ()V public abstract fun setActiveSpan (Lio/sentry/ISpan;)V + public abstract fun setAttribute (Lio/sentry/SentryAttribute;)V + public abstract fun setAttribute (Ljava/lang/String;Ljava/lang/Object;)V + public abstract fun setAttributes (Lio/sentry/SentryAttributes;)V public abstract fun setExtra (Ljava/lang/String;Ljava/lang/String;)V public abstract fun setFingerprint (Ljava/util/List;)V public abstract fun setLevel (Lio/sentry/SentryLevel;)V @@ -1579,10 +1601,14 @@ public final class io/sentry/NoOpHub : io/sentry/IHub { public fun popScope ()V public fun pushIsolationScope ()Lio/sentry/ISentryLifecycleToken; public fun pushScope ()Lio/sentry/ISentryLifecycleToken; + public fun removeAttribute (Ljava/lang/String;)V public fun removeExtra (Ljava/lang/String;)V public fun removeTag (Ljava/lang/String;)V public fun reportFullyDisplayed ()V public fun setActiveSpan (Lio/sentry/ISpan;)V + public fun setAttribute (Lio/sentry/SentryAttribute;)V + public fun setAttribute (Ljava/lang/String;Ljava/lang/Object;)V + public fun setAttributes (Lio/sentry/SentryAttributes;)V public fun setExtra (Ljava/lang/String;Ljava/lang/String;)V public fun setFingerprint (Ljava/util/List;)V public fun setLevel (Lio/sentry/SentryLevel;)V @@ -1649,6 +1675,7 @@ public final class io/sentry/NoOpScope : io/sentry/IScope { public synthetic fun clone ()Ljava/lang/Object; public fun endSession ()Lio/sentry/Session; public fun getAttachments ()Ljava/util/List; + public fun getAttributes ()Ljava/util/Map; public fun getBreadcrumbs ()Ljava/util/Queue; public fun getClient ()Lio/sentry/ISentryClient; public fun getContexts ()Lio/sentry/protocol/Contexts; @@ -1672,11 +1699,15 @@ public final class io/sentry/NoOpScope : io/sentry/IScope { public fun getTransaction ()Lio/sentry/ITransaction; public fun getTransactionName ()Ljava/lang/String; public fun getUser ()Lio/sentry/protocol/User; + public fun removeAttribute (Ljava/lang/String;)V public fun removeContexts (Ljava/lang/String;)V public fun removeExtra (Ljava/lang/String;)V public fun removeTag (Ljava/lang/String;)V public fun replaceOptions (Lio/sentry/SentryOptions;)V public fun setActiveSpan (Lio/sentry/ISpan;)V + public fun setAttribute (Lio/sentry/SentryAttribute;)V + public fun setAttribute (Ljava/lang/String;Ljava/lang/Object;)V + public fun setAttributes (Lio/sentry/SentryAttributes;)V public fun setContexts (Ljava/lang/String;Ljava/lang/Boolean;)V public fun setContexts (Ljava/lang/String;Ljava/lang/Character;)V public fun setContexts (Ljava/lang/String;Ljava/lang/Number;)V @@ -1756,10 +1787,14 @@ public final class io/sentry/NoOpScopes : io/sentry/IScopes { public fun popScope ()V public fun pushIsolationScope ()Lio/sentry/ISentryLifecycleToken; public fun pushScope ()Lio/sentry/ISentryLifecycleToken; + public fun removeAttribute (Ljava/lang/String;)V public fun removeExtra (Ljava/lang/String;)V public fun removeTag (Ljava/lang/String;)V public fun reportFullyDisplayed ()V public fun setActiveSpan (Lio/sentry/ISpan;)V + public fun setAttribute (Lio/sentry/SentryAttribute;)V + public fun setAttribute (Ljava/lang/String;Ljava/lang/Object;)V + public fun setAttributes (Lio/sentry/SentryAttributes;)V public fun setExtra (Ljava/lang/String;Ljava/lang/String;)V public fun setFingerprint (Ljava/util/List;)V public fun setLevel (Lio/sentry/SentryLevel;)V @@ -2325,6 +2360,7 @@ public final class io/sentry/Scope : io/sentry/IScope { public synthetic fun clone ()Ljava/lang/Object; public fun endSession ()Lio/sentry/Session; public fun getAttachments ()Ljava/util/List; + public fun getAttributes ()Ljava/util/Map; public fun getBreadcrumbs ()Ljava/util/Queue; public fun getClient ()Lio/sentry/ISentryClient; public fun getContexts ()Lio/sentry/protocol/Contexts; @@ -2347,11 +2383,15 @@ public final class io/sentry/Scope : io/sentry/IScope { public fun getTransaction ()Lio/sentry/ITransaction; public fun getTransactionName ()Ljava/lang/String; public fun getUser ()Lio/sentry/protocol/User; + public fun removeAttribute (Ljava/lang/String;)V public fun removeContexts (Ljava/lang/String;)V public fun removeExtra (Ljava/lang/String;)V public fun removeTag (Ljava/lang/String;)V public fun replaceOptions (Lio/sentry/SentryOptions;)V public fun setActiveSpan (Lio/sentry/ISpan;)V + public fun setAttribute (Lio/sentry/SentryAttribute;)V + public fun setAttribute (Ljava/lang/String;Ljava/lang/Object;)V + public fun setAttributes (Lio/sentry/SentryAttributes;)V public fun setContexts (Ljava/lang/String;Ljava/lang/Boolean;)V public fun setContexts (Ljava/lang/String;Ljava/lang/Character;)V public fun setContexts (Ljava/lang/String;Ljava/lang/Number;)V @@ -2482,10 +2522,14 @@ public final class io/sentry/Scopes : io/sentry/IScopes { public fun popScope ()V public fun pushIsolationScope ()Lio/sentry/ISentryLifecycleToken; public fun pushScope ()Lio/sentry/ISentryLifecycleToken; + public fun removeAttribute (Ljava/lang/String;)V public fun removeExtra (Ljava/lang/String;)V public fun removeTag (Ljava/lang/String;)V public fun reportFullyDisplayed ()V public fun setActiveSpan (Lio/sentry/ISpan;)V + public fun setAttribute (Lio/sentry/SentryAttribute;)V + public fun setAttribute (Ljava/lang/String;Ljava/lang/Object;)V + public fun setAttributes (Lio/sentry/SentryAttributes;)V public fun setExtra (Ljava/lang/String;Ljava/lang/String;)V public fun setFingerprint (Ljava/util/List;)V public fun setLevel (Lio/sentry/SentryLevel;)V @@ -2555,10 +2599,14 @@ public final class io/sentry/ScopesAdapter : io/sentry/IScopes { public fun popScope ()V public fun pushIsolationScope ()Lio/sentry/ISentryLifecycleToken; public fun pushScope ()Lio/sentry/ISentryLifecycleToken; + public fun removeAttribute (Ljava/lang/String;)V public fun removeExtra (Ljava/lang/String;)V public fun removeTag (Ljava/lang/String;)V public fun reportFullyDisplayed ()V public fun setActiveSpan (Lio/sentry/ISpan;)V + public fun setAttribute (Lio/sentry/SentryAttribute;)V + public fun setAttribute (Ljava/lang/String;Ljava/lang/Object;)V + public fun setAttributes (Lio/sentry/SentryAttributes;)V public fun setExtra (Ljava/lang/String;Ljava/lang/String;)V public fun setFingerprint (Ljava/util/List;)V public fun setLevel (Lio/sentry/SentryLevel;)V @@ -2676,10 +2724,14 @@ public final class io/sentry/Sentry { public static fun popScope ()V public static fun pushIsolationScope ()Lio/sentry/ISentryLifecycleToken; public static fun pushScope ()Lio/sentry/ISentryLifecycleToken; + public static fun removeAttribute (Ljava/lang/String;)V public static fun removeExtra (Ljava/lang/String;)V public static fun removeTag (Ljava/lang/String;)V public static fun replay ()Lio/sentry/IReplayApi; public static fun reportFullyDisplayed ()V + public static fun setAttribute (Lio/sentry/SentryAttribute;)V + public static fun setAttribute (Ljava/lang/String;Ljava/lang/Object;)V + public static fun setAttributes (Lio/sentry/SentryAttributes;)V public static fun setCurrentHub (Lio/sentry/IHub;)Lio/sentry/ISentryLifecycleToken; public static fun setCurrentScopes (Lio/sentry/IScopes;)Lio/sentry/ISentryLifecycleToken; public static fun setExtra (Ljava/lang/String;Ljava/lang/String;)V @@ -2777,6 +2829,7 @@ public final class io/sentry/SentryAttributeType : java/lang/Enum { public static final field INTEGER Lio/sentry/SentryAttributeType; public static final field STRING Lio/sentry/SentryAttributeType; public fun apiName ()Ljava/lang/String; + public static fun inferFrom (Ljava/lang/Object;)Lio/sentry/SentryAttributeType; public static fun valueOf (Ljava/lang/String;)Lio/sentry/SentryAttributeType; public static fun values ()[Lio/sentry/SentryAttributeType; } @@ -3286,6 +3339,7 @@ public final class io/sentry/SentryLogEvent$JsonKeys { public final class io/sentry/SentryLogEventAttributeValue : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun (Lio/sentry/SentryAttributeType;Ljava/lang/Object;)V public fun (Ljava/lang/String;Ljava/lang/Object;)V + public static fun fromAttribute (Lio/sentry/SentryAttribute;)Lio/sentry/SentryLogEventAttributeValue; public fun getType ()Ljava/lang/String; public fun getUnknown ()Ljava/util/Map; public fun getValue ()Ljava/lang/Object; diff --git a/sentry/src/main/java/io/sentry/CombinedScopeView.java b/sentry/src/main/java/io/sentry/CombinedScopeView.java index fc90e6255bd..e04eb722c02 100644 --- a/sentry/src/main/java/io/sentry/CombinedScopeView.java +++ b/sentry/src/main/java/io/sentry/CombinedScopeView.java @@ -14,6 +14,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Queue; @@ -241,6 +242,35 @@ public void removeTag(@Nullable String key) { getDefaultWriteScope().removeTag(key); } + @Override + public @NotNull Map getAttributes() { + final @NotNull Map allAttributes = new HashMap<>(); + allAttributes.putAll(globalScope.getAttributes()); + allAttributes.putAll(isolationScope.getAttributes()); + allAttributes.putAll(scope.getAttributes()); + return allAttributes; + } + + @Override + public void setAttribute(@Nullable String key, @Nullable Object value) { + getDefaultWriteScope().setAttribute(key, value); + } + + @Override + public void setAttribute(@NotNull SentryAttribute attribute) { + getDefaultWriteScope().setAttribute(attribute); + } + + @Override + public void setAttributes(@NotNull SentryAttributes attributes) { + getDefaultWriteScope().setAttributes(attributes); + } + + @Override + public void removeAttribute(@Nullable String key) { + getDefaultWriteScope().removeAttribute(key); + } + @Override public @NotNull Map getExtras() { final @NotNull Map allTags = new ConcurrentHashMap<>(); diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index 5715d061144..4fc6979818d 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -395,6 +395,26 @@ public void reportFullyDisplayed() { return Sentry.getCurrentScopes().metrics(); } + @Override + public void setAttribute(final @Nullable String key, final @Nullable Object value) { + Sentry.setAttribute(key, value); + } + + @Override + public void setAttribute(final @NotNull SentryAttribute attribute) { + Sentry.setAttribute(attribute); + } + + @Override + public void setAttributes(final @NotNull SentryAttributes attributes) { + Sentry.setAttributes(attributes); + } + + @Override + public void removeAttribute(final @Nullable String key) { + Sentry.removeAttribute(key); + } + @Override public void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result) { Sentry.addFeatureFlag(flag, result); diff --git a/sentry/src/main/java/io/sentry/HubScopesWrapper.java b/sentry/src/main/java/io/sentry/HubScopesWrapper.java index c04aad9ed8c..dffd0926bf7 100644 --- a/sentry/src/main/java/io/sentry/HubScopesWrapper.java +++ b/sentry/src/main/java/io/sentry/HubScopesWrapper.java @@ -380,6 +380,26 @@ public void reportFullyDisplayed() { return scopes.metrics(); } + @Override + public void setAttribute(final @Nullable String key, final @Nullable Object value) { + scopes.setAttribute(key, value); + } + + @Override + public void setAttribute(final @NotNull SentryAttribute attribute) { + scopes.setAttribute(attribute); + } + + @Override + public void setAttributes(final @NotNull SentryAttributes attributes) { + scopes.setAttributes(attributes); + } + + @Override + public void removeAttribute(final @Nullable String key) { + scopes.removeAttribute(key); + } + @Override public void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result) { scopes.addFeatureFlag(flag, result); diff --git a/sentry/src/main/java/io/sentry/IScope.java b/sentry/src/main/java/io/sentry/IScope.java index f41ea1cbbe1..1d7763037ac 100644 --- a/sentry/src/main/java/io/sentry/IScope.java +++ b/sentry/src/main/java/io/sentry/IScope.java @@ -425,6 +425,44 @@ void setSpanContext( @ApiStatus.Internal void replaceOptions(final @NotNull SentryOptions options); + /** + * Sets an attribute on the Scope. + * + * @param key the key + * @param value the value + */ + void setAttribute(final @Nullable String key, final @Nullable Object value); + + /** + * Sets an attribute on the Scope. + * + * @param attribute the attribute + */ + void setAttribute(final @NotNull SentryAttribute attribute); + + /** + * Sets multiple attributes on the Scope. + * + * @param attributes the attributes + */ + void setAttributes(final @NotNull SentryAttributes attributes); + + /** + * Removes an attribute from the Scope. + * + * @param key the key + */ + void removeAttribute(final @Nullable String key); + + /** + * Returns the Scope's attributes + * + * @return the attributes map + */ + @ApiStatus.Internal + @NotNull + Map getAttributes(); + void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result); @ApiStatus.Internal diff --git a/sentry/src/main/java/io/sentry/IScopes.java b/sentry/src/main/java/io/sentry/IScopes.java index 0a7c86fa8e6..db7313e5594 100644 --- a/sentry/src/main/java/io/sentry/IScopes.java +++ b/sentry/src/main/java/io/sentry/IScopes.java @@ -748,5 +748,34 @@ default boolean isNoOp() { @NotNull IMetricsApi metrics(); + /** + * Sets an attribute. + * + * @param key the key + * @param value the value + */ + void setAttribute(final @Nullable String key, final @Nullable Object value); + + /** + * Sets an attribute. + * + * @param attribute the attribute + */ + void setAttribute(final @NotNull SentryAttribute attribute); + + /** + * Sets multiple attributes. + * + * @param attributes the attributes + */ + void setAttributes(final @NotNull SentryAttributes attributes); + + /** + * Removes an attribute. + * + * @param key the key + */ + void removeAttribute(final @Nullable String key); + void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result); } diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index 2885d8017d1..39b43745d44 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -338,6 +338,18 @@ public boolean isNoOp() { return NoOpMetricsApi.getInstance(); } + @Override + public void setAttribute(final @Nullable String key, final @Nullable Object value) {} + + @Override + public void setAttribute(final @NotNull SentryAttribute attribute) {} + + @Override + public void setAttributes(final @NotNull SentryAttributes attributes) {} + + @Override + public void removeAttribute(final @Nullable String key) {} + @Override public void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result) {} } diff --git a/sentry/src/main/java/io/sentry/NoOpScope.java b/sentry/src/main/java/io/sentry/NoOpScope.java index c04c5af87bd..34f68bc78cb 100644 --- a/sentry/src/main/java/io/sentry/NoOpScope.java +++ b/sentry/src/main/java/io/sentry/NoOpScope.java @@ -300,6 +300,24 @@ public void setSpanContext( @Override public void replaceOptions(@NotNull SentryOptions options) {} + @Override + public void setAttribute(@Nullable String key, @Nullable Object value) {} + + @Override + public void setAttribute(@NotNull SentryAttribute attribute) {} + + @Override + public void setAttributes(@NotNull SentryAttributes attributes) {} + + @Override + public void removeAttribute(@Nullable String key) {} + + @ApiStatus.Internal + @Override + public @NotNull Map getAttributes() { + return new HashMap<>(); + } + @Override public void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result) {} diff --git a/sentry/src/main/java/io/sentry/NoOpScopes.java b/sentry/src/main/java/io/sentry/NoOpScopes.java index 5abb20226ac..a03a7582b23 100644 --- a/sentry/src/main/java/io/sentry/NoOpScopes.java +++ b/sentry/src/main/java/io/sentry/NoOpScopes.java @@ -336,6 +336,18 @@ public boolean isNoOp() { return NoOpMetricsApi.getInstance(); } + @Override + public void setAttribute(final @Nullable String key, final @Nullable Object value) {} + + @Override + public void setAttribute(final @NotNull SentryAttribute attribute) {} + + @Override + public void setAttributes(final @NotNull SentryAttributes attributes) {} + + @Override + public void removeAttribute(final @Nullable String key) {} + @Override public void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result) {} } diff --git a/sentry/src/main/java/io/sentry/Scope.java b/sentry/src/main/java/io/sentry/Scope.java index 5fc82a648d3..7c926b68df3 100644 --- a/sentry/src/main/java/io/sentry/Scope.java +++ b/sentry/src/main/java/io/sentry/Scope.java @@ -65,6 +65,9 @@ public final class Scope implements IScope { /** Scope's tags */ private @NotNull Map tags = new ConcurrentHashMap<>(); + /** Scope's attributes */ + private @NotNull Map attributes = new ConcurrentHashMap<>(); + /** Scope's extras */ private @NotNull Map extra = new ConcurrentHashMap<>(); @@ -164,6 +167,18 @@ private Scope(final @NotNull Scope scope) { this.tags = tagsClone; + final Map attributesRef = scope.attributes; + + final Map attributesClone = new ConcurrentHashMap<>(); + + for (Map.Entry item : attributesRef.entrySet()) { + if (item != null) { + attributesClone.put(item.getKey(), item.getValue()); // shallow copy + } + } + + this.attributes = attributesClone; + final Map extraRef = scope.extra; Map extraClone = new ConcurrentHashMap<>(); @@ -554,6 +569,7 @@ public void clear() { fingerprint.clear(); clearBreadcrumbs(); tags.clear(); + attributes.clear(); extra.clear(); eventProcessors.clear(); clearTransaction(); @@ -613,6 +629,60 @@ public void removeTag(final @Nullable String key) { } } + /** + * Returns the Scope's attributes + * + * @return the attributes map + */ + @ApiStatus.Internal + @SuppressWarnings("NullAway") // attributes are never null + @Override + public @NotNull Map getAttributes() { + return CollectionUtils.newConcurrentHashMap(attributes); + } + + /** {@inheritDoc} */ + @Override + public void setAttribute(final @Nullable String key, final @Nullable Object value) { + if (key == null) { + return; + } + if (value == null) { + removeAttribute(key); + } else { + this.attributes.put(key, SentryAttribute.named(key, value)); + } + } + + /** {@inheritDoc} */ + @Override + public void setAttribute(final @NotNull SentryAttribute attribute) { + if (attribute == null) { + return; + } + this.attributes.put(attribute.getName(), attribute); + } + + /** {@inheritDoc} */ + @Override + public void setAttributes(final @NotNull SentryAttributes attributes) { + if (attributes == null) { + return; + } + for (SentryAttribute attribute : attributes.getAttributes().values()) { + this.attributes.put(attribute.getName(), attribute); + } + } + + /** {@inheritDoc} */ + @Override + public void removeAttribute(final @Nullable String key) { + if (key == null) { + return; + } + this.attributes.remove(key); + } + /** * Returns the Scope's extra map * diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index 374ecbfdb55..4fbe6040427 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -1229,6 +1229,26 @@ public void reportFullyDisplayed() { return metrics; } + @Override + public void setAttribute(final @Nullable String key, final @Nullable Object value) { + combinedScope.setAttribute(key, value); + } + + @Override + public void setAttribute(final @NotNull SentryAttribute attribute) { + combinedScope.setAttribute(attribute); + } + + @Override + public void setAttributes(final @NotNull SentryAttributes attributes) { + combinedScope.setAttributes(attributes); + } + + @Override + public void removeAttribute(final @Nullable String key) { + combinedScope.removeAttribute(key); + } + @Override public void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result) { combinedScope.addFeatureFlag(flag, result); diff --git a/sentry/src/main/java/io/sentry/ScopesAdapter.java b/sentry/src/main/java/io/sentry/ScopesAdapter.java index ba7e74d23bb..4487a96f22f 100644 --- a/sentry/src/main/java/io/sentry/ScopesAdapter.java +++ b/sentry/src/main/java/io/sentry/ScopesAdapter.java @@ -392,6 +392,26 @@ public void reportFullyDisplayed() { return Sentry.getCurrentScopes().metrics(); } + @Override + public void setAttribute(final @Nullable String key, final @Nullable Object value) { + Sentry.setAttribute(key, value); + } + + @Override + public void setAttribute(final @NotNull SentryAttribute attribute) { + Sentry.setAttribute(attribute); + } + + @Override + public void setAttributes(final @NotNull SentryAttributes attributes) { + Sentry.setAttributes(attributes); + } + + @Override + public void removeAttribute(final @Nullable String key) { + Sentry.removeAttribute(key); + } + @Override public void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result) { Sentry.addFeatureFlag(flag, result); diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index ff84b151658..d50150efa50 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -1368,6 +1368,43 @@ public static void showUserFeedbackDialog( options.getFeedbackOptions().getDialogHandler().showDialog(associatedEventId, configurator); } + /** + * Sets an attribute on the scope. + * + * @param key the key + * @param value the value + */ + public static void setAttribute(final @Nullable String key, final @Nullable Object value) { + getCurrentScopes().setAttribute(key, value); + } + + /** + * Sets an attribute on the scope. + * + * @param attribute the attribute + */ + public static void setAttribute(final @NotNull SentryAttribute attribute) { + getCurrentScopes().setAttribute(attribute); + } + + /** + * Sets multiple attributes on the scope. + * + * @param attributes the attributes + */ + public static void setAttributes(final @NotNull SentryAttributes attributes) { + getCurrentScopes().setAttributes(attributes); + } + + /** + * Removes an attribute from the scope. + * + * @param key the key + */ + public static void removeAttribute(final @Nullable String key) { + getCurrentScopes().removeAttribute(key); + } + public static void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result) { getCurrentScopes().addFeatureFlag(flag, result); } diff --git a/sentry/src/main/java/io/sentry/SentryAttributeType.java b/sentry/src/main/java/io/sentry/SentryAttributeType.java index a47d7e71f0e..8de00d277d2 100644 --- a/sentry/src/main/java/io/sentry/SentryAttributeType.java +++ b/sentry/src/main/java/io/sentry/SentryAttributeType.java @@ -2,6 +2,7 @@ import java.util.Locale; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public enum SentryAttributeType { STRING, @@ -12,4 +13,17 @@ public enum SentryAttributeType { public @NotNull String apiName() { return name().toLowerCase(Locale.ROOT); } + + public static @NotNull SentryAttributeType inferFrom(final @Nullable Object value) { + if (value instanceof Boolean) { + return BOOLEAN; + } + if (value instanceof Integer) { + return INTEGER; + } + if (value instanceof Number) { + return DOUBLE; + } + return STRING; + } } diff --git a/sentry/src/main/java/io/sentry/SentryLogEventAttributeValue.java b/sentry/src/main/java/io/sentry/SentryLogEventAttributeValue.java index 1fd7c8c4528..6f6542927f9 100644 --- a/sentry/src/main/java/io/sentry/SentryLogEventAttributeValue.java +++ b/sentry/src/main/java/io/sentry/SentryLogEventAttributeValue.java @@ -27,6 +27,21 @@ public SentryLogEventAttributeValue( this(type.apiName(), value); } + /** + * Creates a {@link SentryLogEventAttributeValue} from a {@link SentryAttribute}, inferring the + * type if not explicitly set. + * + * @param attribute the attribute + * @return the attribute value + */ + public static @NotNull SentryLogEventAttributeValue fromAttribute( + final @NotNull SentryAttribute attribute) { + final @Nullable Object value = attribute.getValue(); + final @NotNull SentryAttributeType type = + attribute.getType() == null ? SentryAttributeType.inferFrom(value) : attribute.getType(); + return new SentryLogEventAttributeValue(type, value); + } + public @NotNull String getType() { return type; } diff --git a/sentry/src/main/java/io/sentry/logger/LoggerApi.java b/sentry/src/main/java/io/sentry/logger/LoggerApi.java index 37a485df315..c203dcbfb8f 100644 --- a/sentry/src/main/java/io/sentry/logger/LoggerApi.java +++ b/sentry/src/main/java/io/sentry/logger/LoggerApi.java @@ -21,6 +21,7 @@ import io.sentry.util.Platform; import io.sentry.util.TracingUtils; import java.util.HashMap; +import java.util.Map; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -163,6 +164,14 @@ private void captureLog( final @NotNull String message, final @Nullable Object... args) { final @NotNull HashMap attributes = new HashMap<>(); + + final @NotNull Map scopeAttributes = + scopes.getCombinedScopeView().getAttributes(); + for (SentryAttribute scopeAttribute : scopeAttributes.values()) { + attributes.put( + scopeAttribute.getName(), SentryLogEventAttributeValue.fromAttribute(scopeAttribute)); + } + final @NotNull String origin = params.getOrigin(); if (!"manual".equalsIgnoreCase(origin)) { attributes.put( @@ -173,17 +182,14 @@ private void captureLog( if (incomingAttributes != null) { for (SentryAttribute attribute : incomingAttributes.getAttributes().values()) { - final @Nullable Object value = attribute.getValue(); - final @NotNull SentryAttributeType type = - attribute.getType() == null ? getType(value) : attribute.getType(); - attributes.put(attribute.getName(), new SentryLogEventAttributeValue(type, value)); + attributes.put(attribute.getName(), SentryLogEventAttributeValue.fromAttribute(attribute)); } } if (args != null) { int i = 0; for (Object arg : args) { - final @NotNull SentryAttributeType type = getType(arg); + final @NotNull SentryAttributeType type = SentryAttributeType.inferFrom(arg); attributes.put( "sentry.message.parameter." + i, new SentryLogEventAttributeValue(type, arg)); i++; @@ -292,17 +298,4 @@ private void setUser(final @NotNull HashMap createAttributes( final @NotNull SentryMetricsParameters params) { final @NotNull HashMap attributes = new HashMap<>(); + + final @NotNull Map scopeAttributes = + scopes.getCombinedScopeView().getAttributes(); + for (SentryAttribute scopeAttribute : scopeAttributes.values()) { + attributes.put( + scopeAttribute.getName(), SentryLogEventAttributeValue.fromAttribute(scopeAttribute)); + } + final @NotNull String origin = params.getOrigin(); if (!"manual".equalsIgnoreCase(origin)) { attributes.put( @@ -177,10 +186,7 @@ private void captureMetrics( if (incomingAttributes != null) { for (SentryAttribute attribute : incomingAttributes.getAttributes().values()) { - final @Nullable Object value = attribute.getValue(); - final @NotNull SentryAttributeType type = - attribute.getType() == null ? getType(value) : attribute.getType(); - attributes.put(attribute.getName(), new SentryLogEventAttributeValue(type, value)); + attributes.put(attribute.getName(), SentryLogEventAttributeValue.fromAttribute(attribute)); } } @@ -279,17 +285,4 @@ private void setUser(final @NotNull HashMap Date: Thu, 26 Feb 2026 10:53:52 +0100 Subject: [PATCH 02/20] changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index df25c4b000f..2deaf3b9274 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Add API to set attributes on scope that are automatically included in logs and metrics ([#5118](https://github.com/getsentry/sentry-java/pull/5118)) + ### Fixes - Fix crash when unregistering `SystemEventsBroadcastReceiver` with try-catch block. ([#5106](https://github.com/getsentry/sentry-java/pull/5106)) From de80a2357cb20c6fdf663e55409dafa732c93d56 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 26 Feb 2026 11:40:22 +0100 Subject: [PATCH 03/20] ref: Split out LoggerApi/MetricsApi changes for stacked PR Move factory method extractions (SentryAttributeType.inferFrom, SentryLogEventAttributeValue.fromAttribute) and LoggerApi/MetricsApi scope attribute integration to a separate stacked PR. Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 2 +- sentry/api/sentry.api | 2 -- .../java/io/sentry/SentryAttributeType.java | 14 --------- .../sentry/SentryLogEventAttributeValue.java | 15 ---------- .../main/java/io/sentry/logger/LoggerApi.java | 29 ++++++++++++------- .../java/io/sentry/metrics/MetricsApi.java | 27 ++++++++++------- 6 files changed, 36 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2deaf3b9274..46de652259a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features -- Add API to set attributes on scope that are automatically included in logs and metrics ([#5118](https://github.com/getsentry/sentry-java/pull/5118)) +- Add scope-level attributes API ([#5118](https://github.com/getsentry/sentry-java/pull/5118)) ### Fixes diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 9a7d360fce4..6cb68d777f8 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2829,7 +2829,6 @@ public final class io/sentry/SentryAttributeType : java/lang/Enum { public static final field INTEGER Lio/sentry/SentryAttributeType; public static final field STRING Lio/sentry/SentryAttributeType; public fun apiName ()Ljava/lang/String; - public static fun inferFrom (Ljava/lang/Object;)Lio/sentry/SentryAttributeType; public static fun valueOf (Ljava/lang/String;)Lio/sentry/SentryAttributeType; public static fun values ()[Lio/sentry/SentryAttributeType; } @@ -3339,7 +3338,6 @@ public final class io/sentry/SentryLogEvent$JsonKeys { public final class io/sentry/SentryLogEventAttributeValue : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun (Lio/sentry/SentryAttributeType;Ljava/lang/Object;)V public fun (Ljava/lang/String;Ljava/lang/Object;)V - public static fun fromAttribute (Lio/sentry/SentryAttribute;)Lio/sentry/SentryLogEventAttributeValue; public fun getType ()Ljava/lang/String; public fun getUnknown ()Ljava/util/Map; public fun getValue ()Ljava/lang/Object; diff --git a/sentry/src/main/java/io/sentry/SentryAttributeType.java b/sentry/src/main/java/io/sentry/SentryAttributeType.java index 8de00d277d2..a47d7e71f0e 100644 --- a/sentry/src/main/java/io/sentry/SentryAttributeType.java +++ b/sentry/src/main/java/io/sentry/SentryAttributeType.java @@ -2,7 +2,6 @@ import java.util.Locale; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; public enum SentryAttributeType { STRING, @@ -13,17 +12,4 @@ public enum SentryAttributeType { public @NotNull String apiName() { return name().toLowerCase(Locale.ROOT); } - - public static @NotNull SentryAttributeType inferFrom(final @Nullable Object value) { - if (value instanceof Boolean) { - return BOOLEAN; - } - if (value instanceof Integer) { - return INTEGER; - } - if (value instanceof Number) { - return DOUBLE; - } - return STRING; - } } diff --git a/sentry/src/main/java/io/sentry/SentryLogEventAttributeValue.java b/sentry/src/main/java/io/sentry/SentryLogEventAttributeValue.java index 6f6542927f9..1fd7c8c4528 100644 --- a/sentry/src/main/java/io/sentry/SentryLogEventAttributeValue.java +++ b/sentry/src/main/java/io/sentry/SentryLogEventAttributeValue.java @@ -27,21 +27,6 @@ public SentryLogEventAttributeValue( this(type.apiName(), value); } - /** - * Creates a {@link SentryLogEventAttributeValue} from a {@link SentryAttribute}, inferring the - * type if not explicitly set. - * - * @param attribute the attribute - * @return the attribute value - */ - public static @NotNull SentryLogEventAttributeValue fromAttribute( - final @NotNull SentryAttribute attribute) { - final @Nullable Object value = attribute.getValue(); - final @NotNull SentryAttributeType type = - attribute.getType() == null ? SentryAttributeType.inferFrom(value) : attribute.getType(); - return new SentryLogEventAttributeValue(type, value); - } - public @NotNull String getType() { return type; } diff --git a/sentry/src/main/java/io/sentry/logger/LoggerApi.java b/sentry/src/main/java/io/sentry/logger/LoggerApi.java index c203dcbfb8f..37a485df315 100644 --- a/sentry/src/main/java/io/sentry/logger/LoggerApi.java +++ b/sentry/src/main/java/io/sentry/logger/LoggerApi.java @@ -21,7 +21,6 @@ import io.sentry.util.Platform; import io.sentry.util.TracingUtils; import java.util.HashMap; -import java.util.Map; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -164,14 +163,6 @@ private void captureLog( final @NotNull String message, final @Nullable Object... args) { final @NotNull HashMap attributes = new HashMap<>(); - - final @NotNull Map scopeAttributes = - scopes.getCombinedScopeView().getAttributes(); - for (SentryAttribute scopeAttribute : scopeAttributes.values()) { - attributes.put( - scopeAttribute.getName(), SentryLogEventAttributeValue.fromAttribute(scopeAttribute)); - } - final @NotNull String origin = params.getOrigin(); if (!"manual".equalsIgnoreCase(origin)) { attributes.put( @@ -182,14 +173,17 @@ private void captureLog( if (incomingAttributes != null) { for (SentryAttribute attribute : incomingAttributes.getAttributes().values()) { - attributes.put(attribute.getName(), SentryLogEventAttributeValue.fromAttribute(attribute)); + final @Nullable Object value = attribute.getValue(); + final @NotNull SentryAttributeType type = + attribute.getType() == null ? getType(value) : attribute.getType(); + attributes.put(attribute.getName(), new SentryLogEventAttributeValue(type, value)); } } if (args != null) { int i = 0; for (Object arg : args) { - final @NotNull SentryAttributeType type = SentryAttributeType.inferFrom(arg); + final @NotNull SentryAttributeType type = getType(arg); attributes.put( "sentry.message.parameter." + i, new SentryLogEventAttributeValue(type, arg)); i++; @@ -298,4 +292,17 @@ private void setUser(final @NotNull HashMap createAttributes( final @NotNull SentryMetricsParameters params) { final @NotNull HashMap attributes = new HashMap<>(); - - final @NotNull Map scopeAttributes = - scopes.getCombinedScopeView().getAttributes(); - for (SentryAttribute scopeAttribute : scopeAttributes.values()) { - attributes.put( - scopeAttribute.getName(), SentryLogEventAttributeValue.fromAttribute(scopeAttribute)); - } - final @NotNull String origin = params.getOrigin(); if (!"manual".equalsIgnoreCase(origin)) { attributes.put( @@ -186,7 +177,10 @@ private void captureMetrics( if (incomingAttributes != null) { for (SentryAttribute attribute : incomingAttributes.getAttributes().values()) { - attributes.put(attribute.getName(), SentryLogEventAttributeValue.fromAttribute(attribute)); + final @Nullable Object value = attribute.getValue(); + final @NotNull SentryAttributeType type = + attribute.getType() == null ? getType(value) : attribute.getType(); + attributes.put(attribute.getName(), new SentryLogEventAttributeValue(type, value)); } } @@ -285,4 +279,17 @@ private void setUser(final @NotNull HashMap Date: Thu, 26 Feb 2026 11:47:55 +0100 Subject: [PATCH 04/20] feat(core): Wire scope attributes into LoggerApi and MetricsApi Extract factory methods SentryAttributeType.inferFrom and SentryLogEventAttributeValue.fromAttribute to reduce duplication. Apply scope attributes to log and metric events automatically. Co-Authored-By: Claude Opus 4.6 --- sentry/api/sentry.api | 2 ++ .../java/io/sentry/SentryAttributeType.java | 14 +++++++++ .../sentry/SentryLogEventAttributeValue.java | 15 ++++++++++ .../main/java/io/sentry/logger/LoggerApi.java | 29 +++++++------------ .../java/io/sentry/metrics/MetricsApi.java | 27 +++++++---------- 5 files changed, 52 insertions(+), 35 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 6cb68d777f8..9a7d360fce4 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2829,6 +2829,7 @@ public final class io/sentry/SentryAttributeType : java/lang/Enum { public static final field INTEGER Lio/sentry/SentryAttributeType; public static final field STRING Lio/sentry/SentryAttributeType; public fun apiName ()Ljava/lang/String; + public static fun inferFrom (Ljava/lang/Object;)Lio/sentry/SentryAttributeType; public static fun valueOf (Ljava/lang/String;)Lio/sentry/SentryAttributeType; public static fun values ()[Lio/sentry/SentryAttributeType; } @@ -3338,6 +3339,7 @@ public final class io/sentry/SentryLogEvent$JsonKeys { public final class io/sentry/SentryLogEventAttributeValue : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun (Lio/sentry/SentryAttributeType;Ljava/lang/Object;)V public fun (Ljava/lang/String;Ljava/lang/Object;)V + public static fun fromAttribute (Lio/sentry/SentryAttribute;)Lio/sentry/SentryLogEventAttributeValue; public fun getType ()Ljava/lang/String; public fun getUnknown ()Ljava/util/Map; public fun getValue ()Ljava/lang/Object; diff --git a/sentry/src/main/java/io/sentry/SentryAttributeType.java b/sentry/src/main/java/io/sentry/SentryAttributeType.java index a47d7e71f0e..8de00d277d2 100644 --- a/sentry/src/main/java/io/sentry/SentryAttributeType.java +++ b/sentry/src/main/java/io/sentry/SentryAttributeType.java @@ -2,6 +2,7 @@ import java.util.Locale; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public enum SentryAttributeType { STRING, @@ -12,4 +13,17 @@ public enum SentryAttributeType { public @NotNull String apiName() { return name().toLowerCase(Locale.ROOT); } + + public static @NotNull SentryAttributeType inferFrom(final @Nullable Object value) { + if (value instanceof Boolean) { + return BOOLEAN; + } + if (value instanceof Integer) { + return INTEGER; + } + if (value instanceof Number) { + return DOUBLE; + } + return STRING; + } } diff --git a/sentry/src/main/java/io/sentry/SentryLogEventAttributeValue.java b/sentry/src/main/java/io/sentry/SentryLogEventAttributeValue.java index 1fd7c8c4528..6f6542927f9 100644 --- a/sentry/src/main/java/io/sentry/SentryLogEventAttributeValue.java +++ b/sentry/src/main/java/io/sentry/SentryLogEventAttributeValue.java @@ -27,6 +27,21 @@ public SentryLogEventAttributeValue( this(type.apiName(), value); } + /** + * Creates a {@link SentryLogEventAttributeValue} from a {@link SentryAttribute}, inferring the + * type if not explicitly set. + * + * @param attribute the attribute + * @return the attribute value + */ + public static @NotNull SentryLogEventAttributeValue fromAttribute( + final @NotNull SentryAttribute attribute) { + final @Nullable Object value = attribute.getValue(); + final @NotNull SentryAttributeType type = + attribute.getType() == null ? SentryAttributeType.inferFrom(value) : attribute.getType(); + return new SentryLogEventAttributeValue(type, value); + } + public @NotNull String getType() { return type; } diff --git a/sentry/src/main/java/io/sentry/logger/LoggerApi.java b/sentry/src/main/java/io/sentry/logger/LoggerApi.java index 37a485df315..c203dcbfb8f 100644 --- a/sentry/src/main/java/io/sentry/logger/LoggerApi.java +++ b/sentry/src/main/java/io/sentry/logger/LoggerApi.java @@ -21,6 +21,7 @@ import io.sentry.util.Platform; import io.sentry.util.TracingUtils; import java.util.HashMap; +import java.util.Map; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -163,6 +164,14 @@ private void captureLog( final @NotNull String message, final @Nullable Object... args) { final @NotNull HashMap attributes = new HashMap<>(); + + final @NotNull Map scopeAttributes = + scopes.getCombinedScopeView().getAttributes(); + for (SentryAttribute scopeAttribute : scopeAttributes.values()) { + attributes.put( + scopeAttribute.getName(), SentryLogEventAttributeValue.fromAttribute(scopeAttribute)); + } + final @NotNull String origin = params.getOrigin(); if (!"manual".equalsIgnoreCase(origin)) { attributes.put( @@ -173,17 +182,14 @@ private void captureLog( if (incomingAttributes != null) { for (SentryAttribute attribute : incomingAttributes.getAttributes().values()) { - final @Nullable Object value = attribute.getValue(); - final @NotNull SentryAttributeType type = - attribute.getType() == null ? getType(value) : attribute.getType(); - attributes.put(attribute.getName(), new SentryLogEventAttributeValue(type, value)); + attributes.put(attribute.getName(), SentryLogEventAttributeValue.fromAttribute(attribute)); } } if (args != null) { int i = 0; for (Object arg : args) { - final @NotNull SentryAttributeType type = getType(arg); + final @NotNull SentryAttributeType type = SentryAttributeType.inferFrom(arg); attributes.put( "sentry.message.parameter." + i, new SentryLogEventAttributeValue(type, arg)); i++; @@ -292,17 +298,4 @@ private void setUser(final @NotNull HashMap createAttributes( final @NotNull SentryMetricsParameters params) { final @NotNull HashMap attributes = new HashMap<>(); + + final @NotNull Map scopeAttributes = + scopes.getCombinedScopeView().getAttributes(); + for (SentryAttribute scopeAttribute : scopeAttributes.values()) { + attributes.put( + scopeAttribute.getName(), SentryLogEventAttributeValue.fromAttribute(scopeAttribute)); + } + final @NotNull String origin = params.getOrigin(); if (!"manual".equalsIgnoreCase(origin)) { attributes.put( @@ -177,10 +186,7 @@ private void captureMetrics( if (incomingAttributes != null) { for (SentryAttribute attribute : incomingAttributes.getAttributes().values()) { - final @Nullable Object value = attribute.getValue(); - final @NotNull SentryAttributeType type = - attribute.getType() == null ? getType(value) : attribute.getType(); - attributes.put(attribute.getName(), new SentryLogEventAttributeValue(type, value)); + attributes.put(attribute.getName(), SentryLogEventAttributeValue.fromAttribute(attribute)); } } @@ -279,17 +285,4 @@ private void setUser(final @NotNull HashMap Date: Thu, 26 Feb 2026 11:50:54 +0100 Subject: [PATCH 05/20] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46de652259a..5114beb919e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - 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)) ### Fixes From 5974800ab21fef54525c6c8c8fbb974d3437573f Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 26 Feb 2026 13:08:11 +0100 Subject: [PATCH 06/20] feat(samples): Showcase scope attributes in Spring Boot 4 samples Add Sentry.setAttribute() calls to PersonController and MetricController across all Spring Boot 4 sample variants to demonstrate scope attributes being auto-attached to logs and metrics. Add e2e test assertions and TestHelper methods to verify scope attributes appear on captured log and metric events. Co-Authored-By: Claude Opus 4.6 --- .../spring/boot4/MetricController.java | 3 + .../spring/boot4/PersonController.java | 5 ++ .../io/sentry/systemtest/MetricsSystemTest.kt | 4 +- .../io/sentry/systemtest/PersonSystemTest.kt | 15 ++++- .../spring/boot4/MetricController.java | 3 + .../spring/boot4/PersonController.java | 5 ++ .../io/sentry/systemtest/MetricsSystemTest.kt | 4 +- .../io/sentry/systemtest/PersonSystemTest.kt | 15 ++++- .../spring/boot4/MetricController.java | 3 + .../spring/boot4/PersonController.java | 5 ++ .../io/sentry/systemtest/MetricsSystemTest.kt | 4 +- .../io/sentry/systemtest/PersonSystemTest.kt | 15 ++++- .../spring/boot4/MetricController.java | 2 + .../spring/boot4/PersonController.java | 4 ++ .../io/sentry/systemtest/MetricsSystemTest.kt | 4 +- .../io/sentry/systemtest/PersonSystemTest.kt | 15 ++++- .../api/sentry-system-test-support.api | 2 + .../io/sentry/systemtest/util/TestHelper.kt | 62 +++++++++++++++++++ 18 files changed, 162 insertions(+), 8 deletions(-) diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/MetricController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/MetricController.java index 2a969ec8849..5088aa6c7c0 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/MetricController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/MetricController.java @@ -16,6 +16,9 @@ public class MetricController { @GetMapping("count") String count() { + // Set scope attributes - these are automatically attached to metrics + Sentry.setAttribute("user.type", "admin"); + Sentry.setAttribute("feature.version", 2); Sentry.metrics().count("countMetric"); return "count metric increased"; } diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/PersonController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/PersonController.java index b96c840aae8..6cf311f47dc 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/PersonController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/PersonController.java @@ -34,6 +34,11 @@ Person person(@PathVariable Long id) { Sentry.addFeatureFlag("outer-feature-flag", true); Span span = tracer.spanBuilder("spanCreatedThroughOtelApi").startSpan(); try (final @NotNull Scope spanScope = span.makeCurrent()) { + // Set scope attributes - these are automatically attached to logs and metrics + Sentry.setAttribute("user.type", "admin"); + Sentry.setAttribute("feature.version", 2); + Sentry.setAttribute("debug.enabled", true); + Sentry.logger().warn("warn Sentry logging"); Sentry.logger().error("error Sentry logging"); Sentry.logger().info("hello %s %s", "there", "world!"); diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt index dc2ca2a10ae..039d9d640c7 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -21,7 +21,9 @@ class MetricsSystemTest { assertEquals(200, restClient.lastKnownStatusCode) testHelper.ensureMetricsReceived { event, header -> - testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) + testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) && + testHelper.doesMetricHaveAttribute(event, "countMetric", "user.type", "admin") && + testHelper.doesMetricHaveAttribute(event, "countMetric", "feature.version", 2) } } diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt index 50bc732b657..1fe742b64ce 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -56,7 +56,20 @@ class PersonSystemTest { testHelper.ensureLogsReceived { logs, envelopeHeader -> testHelper.doesContainLogWithBody(logs, "warn Sentry logging") && testHelper.doesContainLogWithBody(logs, "error Sentry logging") && - testHelper.doesContainLogWithBody(logs, "hello there world!") + testHelper.doesContainLogWithBody(logs, "hello there world!") && + testHelper.doesLogWithBodyHaveAttribute( + logs, + "warn Sentry logging", + "user.type", + "admin", + ) && + testHelper.doesLogWithBodyHaveAttribute( + logs, + "warn Sentry logging", + "feature.version", + 2, + ) && + testHelper.doesLogWithBodyHaveAttribute(logs, "warn Sentry logging", "debug.enabled", true) } } diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/MetricController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/MetricController.java index 2a969ec8849..5088aa6c7c0 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/MetricController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/MetricController.java @@ -16,6 +16,9 @@ public class MetricController { @GetMapping("count") String count() { + // Set scope attributes - these are automatically attached to metrics + Sentry.setAttribute("user.type", "admin"); + Sentry.setAttribute("feature.version", 2); Sentry.metrics().count("countMetric"); return "count metric increased"; } diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/PersonController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/PersonController.java index bde91c83825..a3d17dd2d93 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/PersonController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/PersonController.java @@ -32,6 +32,11 @@ Person person(@PathVariable Long id) { Sentry.addFeatureFlag("transaction-feature-flag", true); Span span = tracer.spanBuilder("spanCreatedThroughOtelApi").startSpan(); try (final @NotNull Scope spanScope = span.makeCurrent()) { + // Set scope attributes - these are automatically attached to logs and metrics + Sentry.setAttribute("user.type", "admin"); + Sentry.setAttribute("feature.version", 2); + Sentry.setAttribute("debug.enabled", true); + Sentry.logger().warn("warn Sentry logging"); Sentry.logger().error("error Sentry logging"); Sentry.logger().info("hello %s %s", "there", "world!"); diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt index dc2ca2a10ae..039d9d640c7 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -21,7 +21,9 @@ class MetricsSystemTest { assertEquals(200, restClient.lastKnownStatusCode) testHelper.ensureMetricsReceived { event, header -> - testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) + testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) && + testHelper.doesMetricHaveAttribute(event, "countMetric", "user.type", "admin") && + testHelper.doesMetricHaveAttribute(event, "countMetric", "feature.version", 2) } } diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt index a4d7cc5bdc5..ad9b5f77b62 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -51,7 +51,20 @@ class PersonSystemTest { testHelper.ensureLogsReceived { logs, envelopeHeader -> testHelper.doesContainLogWithBody(logs, "warn Sentry logging") && testHelper.doesContainLogWithBody(logs, "error Sentry logging") && - testHelper.doesContainLogWithBody(logs, "hello there world!") + testHelper.doesContainLogWithBody(logs, "hello there world!") && + testHelper.doesLogWithBodyHaveAttribute( + logs, + "warn Sentry logging", + "user.type", + "admin", + ) && + testHelper.doesLogWithBodyHaveAttribute( + logs, + "warn Sentry logging", + "feature.version", + 2, + ) && + testHelper.doesLogWithBodyHaveAttribute(logs, "warn Sentry logging", "debug.enabled", true) } } diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/MetricController.java b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/MetricController.java index 2a969ec8849..5088aa6c7c0 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/MetricController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/MetricController.java @@ -16,6 +16,9 @@ public class MetricController { @GetMapping("count") String count() { + // Set scope attributes - these are automatically attached to metrics + Sentry.setAttribute("user.type", "admin"); + Sentry.setAttribute("feature.version", 2); Sentry.metrics().count("countMetric"); return "count metric increased"; } diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/PersonController.java b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/PersonController.java index 0db43f5ab71..cb23c1b1b3f 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/PersonController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/PersonController.java @@ -23,6 +23,11 @@ public PersonController(PersonService personService) { @GetMapping("{id}") Person person(@PathVariable Long id) { + // Set scope attributes - these are automatically attached to logs and metrics + Sentry.setAttribute("user.type", "admin"); + Sentry.setAttribute("feature.version", 2); + Sentry.setAttribute("debug.enabled", true); + Sentry.logger().warn("warn Sentry logging"); Sentry.logger().error("error Sentry logging"); Sentry.logger().info("hello %s %s", "there", "world!"); diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt index dc2ca2a10ae..039d9d640c7 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -21,7 +21,9 @@ class MetricsSystemTest { assertEquals(200, restClient.lastKnownStatusCode) testHelper.ensureMetricsReceived { event, header -> - testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) + testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) && + testHelper.doesMetricHaveAttribute(event, "countMetric", "user.type", "admin") && + testHelper.doesMetricHaveAttribute(event, "countMetric", "feature.version", 2) } } diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt index 7ba241200d4..3b7b2751ee9 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -35,7 +35,20 @@ class PersonSystemTest { testHelper.ensureLogsReceived { logs, envelopeHeader -> testHelper.doesContainLogWithBody(logs, "warn Sentry logging") && testHelper.doesContainLogWithBody(logs, "error Sentry logging") && - testHelper.doesContainLogWithBody(logs, "hello there world!") + testHelper.doesContainLogWithBody(logs, "hello there world!") && + testHelper.doesLogWithBodyHaveAttribute( + logs, + "warn Sentry logging", + "user.type", + "admin", + ) && + testHelper.doesLogWithBodyHaveAttribute( + logs, + "warn Sentry logging", + "feature.version", + 2, + ) && + testHelper.doesLogWithBodyHaveAttribute(logs, "warn Sentry logging", "debug.enabled", true) } } diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/MetricController.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/MetricController.java index 2a969ec8849..be75f5e3002 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/MetricController.java +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/MetricController.java @@ -16,6 +16,8 @@ public class MetricController { @GetMapping("count") String count() { + Sentry.setAttribute("user.type", "admin"); + Sentry.setAttribute("feature.version", 2); Sentry.metrics().count("countMetric"); return "count metric increased"; } diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/PersonController.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/PersonController.java index c65c9040d5c..489dc629d28 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/PersonController.java +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/PersonController.java @@ -27,6 +27,10 @@ Person person(@PathVariable Long id) { ISpan currentSpan = Sentry.getSpan(); ISpan sentrySpan = currentSpan.startChild("spanCreatedThroughSentryApi"); try { + Sentry.setAttribute("user.type", "admin"); + Sentry.setAttribute("feature.version", 2); + Sentry.setAttribute("debug.enabled", true); + Sentry.logger().warn("warn Sentry logging"); Sentry.logger().error("error Sentry logging"); Sentry.logger().info("hello %s %s", "there", "world!"); diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt index dc2ca2a10ae..039d9d640c7 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -21,7 +21,9 @@ class MetricsSystemTest { assertEquals(200, restClient.lastKnownStatusCode) testHelper.ensureMetricsReceived { event, header -> - testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) + testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) && + testHelper.doesMetricHaveAttribute(event, "countMetric", "user.type", "admin") && + testHelper.doesMetricHaveAttribute(event, "countMetric", "feature.version", 2) } } diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt index 362a8577148..2389734a8b3 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -50,7 +50,20 @@ class PersonSystemTest { testHelper.ensureLogsReceived { logs, envelopeHeader -> testHelper.doesContainLogWithBody(logs, "warn Sentry logging") && testHelper.doesContainLogWithBody(logs, "error Sentry logging") && - testHelper.doesContainLogWithBody(logs, "hello there world!") + testHelper.doesContainLogWithBody(logs, "hello there world!") && + testHelper.doesLogWithBodyHaveAttribute( + logs, + "warn Sentry logging", + "user.type", + "admin", + ) && + testHelper.doesLogWithBodyHaveAttribute( + logs, + "warn Sentry logging", + "feature.version", + 2, + ) && + testHelper.doesLogWithBodyHaveAttribute(logs, "warn Sentry logging", "debug.enabled", true) } } diff --git a/sentry-system-test-support/api/sentry-system-test-support.api b/sentry-system-test-support/api/sentry-system-test-support.api index ff620c7d809..51ef7da55d9 100644 --- a/sentry-system-test-support/api/sentry-system-test-support.api +++ b/sentry-system-test-support/api/sentry-system-test-support.api @@ -574,6 +574,8 @@ public final class io/sentry/systemtest/util/TestHelper { public static synthetic fun doesContainMetric$default (Lio/sentry/systemtest/util/TestHelper;Lio/sentry/SentryMetricsEvents;Ljava/lang/String;Ljava/lang/String;DLjava/lang/String;ILjava/lang/Object;)Z public final fun doesEventHaveExceptionMessage (Lio/sentry/SentryEvent;Ljava/lang/String;)Z public final fun doesEventHaveFlag (Lio/sentry/SentryEvent;Ljava/lang/String;Z)Z + public final fun doesLogWithBodyHaveAttribute (Lio/sentry/SentryLogEvents;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)Z + public final fun doesMetricHaveAttribute (Lio/sentry/SentryMetricsEvents;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)Z public final fun doesTransactionContainSpanWithDescription (Lio/sentry/protocol/SentryTransaction;Ljava/lang/String;)Z public final fun doesTransactionContainSpanWithOp (Lio/sentry/protocol/SentryTransaction;Ljava/lang/String;)Z public final fun doesTransactionContainSpanWithOpAndDescription (Lio/sentry/protocol/SentryTransaction;Ljava/lang/String;Ljava/lang/String;)Z diff --git a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt index 2881460a2d0..19817c34ac8 100644 --- a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt +++ b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt @@ -190,6 +190,68 @@ class TestHelper(backendUrl: String) { return true } + fun doesLogWithBodyHaveAttribute( + logs: SentryLogEvents, + body: String, + attributeKey: String, + attributeValue: Any?, + ): Boolean { + val logItem = logs.items.firstOrNull { logItem -> logItem.body == body } + if (logItem == null) { + println("Unable to find log item with body $body in logs:") + logObject(logs) + return false + } + + val attr = logItem.attributes?.get(attributeKey) + if (attr == null) { + println("Unable to find attribute $attributeKey on log with body $body:") + logObject(logItem) + return false + } + + if (attr.value != attributeValue) { + println( + "Attribute $attributeKey has value ${attr.value} but expected $attributeValue on log with body $body:" + ) + logObject(logItem) + return false + } + + return true + } + + fun doesMetricHaveAttribute( + metrics: SentryMetricsEvents, + metricName: String, + attributeKey: String, + attributeValue: Any?, + ): Boolean { + val metricItem = metrics.items.firstOrNull { it.name == metricName } + if (metricItem == null) { + println("Unable to find metric with name $metricName in metrics:") + logObject(metrics) + return false + } + + val attr = metricItem.attributes?.get(attributeKey) + if (attr == null) { + println("Unable to find attribute $attributeKey on metric $metricName:") + logObject(metricItem) + return false + } + + if (attr.value != attributeValue) { + println( + "Attribute $attributeKey has value ${attr.value} but expected $attributeValue on metric $metricName:" + ) + logObject(metricItem) + return false + } + + return true + } + private fun checkIfTransactionMatches( envelopeString: String, callback: ((SentryTransaction, SentryEnvelopeHeader) -> Boolean), From 7189bdca1a211085608f16bf3443c9e6675b6680 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 26 Feb 2026 13:18:10 +0100 Subject: [PATCH 07/20] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5114beb919e..32cfeaa1279 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - 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)) +- Showcase scope attributes in Spring Boot 4 samples with e2e tests ([#5121](https://github.com/getsentry/sentry-java/pull/5121)) ### Fixes From 082fab091b03f366defb21cc3b54fb9dce4c72f7 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 26 Feb 2026 13:18:37 +0100 Subject: [PATCH 08/20] Revert "changelog" This reverts commit 7189bdca1a211085608f16bf3443c9e6675b6680. --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32cfeaa1279..5114beb919e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,6 @@ - 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)) -- Showcase scope attributes in Spring Boot 4 samples with e2e tests ([#5121](https://github.com/getsentry/sentry-java/pull/5121)) ### Fixes From f0a2a2154eceb63a70b53f5d7e94e4977307b418 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 26 Feb 2026 13:35:30 +0100 Subject: [PATCH 09/20] ref: Remove redundant comments from variant controllers Co-Authored-By: Claude Opus 4.6 --- .../java/io/sentry/samples/spring/boot4/MetricController.java | 1 - .../java/io/sentry/samples/spring/boot4/PersonController.java | 1 - .../java/io/sentry/samples/spring/boot4/MetricController.java | 1 - .../java/io/sentry/samples/spring/boot4/PersonController.java | 1 - .../java/io/sentry/samples/spring/boot4/MetricController.java | 1 - .../java/io/sentry/samples/spring/boot4/PersonController.java | 1 - 6 files changed, 6 deletions(-) diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/MetricController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/MetricController.java index 5088aa6c7c0..be75f5e3002 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/MetricController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/MetricController.java @@ -16,7 +16,6 @@ public class MetricController { @GetMapping("count") String count() { - // Set scope attributes - these are automatically attached to metrics Sentry.setAttribute("user.type", "admin"); Sentry.setAttribute("feature.version", 2); Sentry.metrics().count("countMetric"); diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/PersonController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/PersonController.java index 6cf311f47dc..c434d67ceb3 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/PersonController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/PersonController.java @@ -34,7 +34,6 @@ Person person(@PathVariable Long id) { Sentry.addFeatureFlag("outer-feature-flag", true); Span span = tracer.spanBuilder("spanCreatedThroughOtelApi").startSpan(); try (final @NotNull Scope spanScope = span.makeCurrent()) { - // Set scope attributes - these are automatically attached to logs and metrics Sentry.setAttribute("user.type", "admin"); Sentry.setAttribute("feature.version", 2); Sentry.setAttribute("debug.enabled", true); diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/MetricController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/MetricController.java index 5088aa6c7c0..be75f5e3002 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/MetricController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/MetricController.java @@ -16,7 +16,6 @@ public class MetricController { @GetMapping("count") String count() { - // Set scope attributes - these are automatically attached to metrics Sentry.setAttribute("user.type", "admin"); Sentry.setAttribute("feature.version", 2); Sentry.metrics().count("countMetric"); diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/PersonController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/PersonController.java index a3d17dd2d93..9b5c9fc3819 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/PersonController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/PersonController.java @@ -32,7 +32,6 @@ Person person(@PathVariable Long id) { Sentry.addFeatureFlag("transaction-feature-flag", true); Span span = tracer.spanBuilder("spanCreatedThroughOtelApi").startSpan(); try (final @NotNull Scope spanScope = span.makeCurrent()) { - // Set scope attributes - these are automatically attached to logs and metrics Sentry.setAttribute("user.type", "admin"); Sentry.setAttribute("feature.version", 2); Sentry.setAttribute("debug.enabled", true); diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/MetricController.java b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/MetricController.java index 5088aa6c7c0..be75f5e3002 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/MetricController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/MetricController.java @@ -16,7 +16,6 @@ public class MetricController { @GetMapping("count") String count() { - // Set scope attributes - these are automatically attached to metrics Sentry.setAttribute("user.type", "admin"); Sentry.setAttribute("feature.version", 2); Sentry.metrics().count("countMetric"); diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/PersonController.java b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/PersonController.java index cb23c1b1b3f..e5e00f27846 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/PersonController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/PersonController.java @@ -23,7 +23,6 @@ public PersonController(PersonService personService) { @GetMapping("{id}") Person person(@PathVariable Long id) { - // Set scope attributes - these are automatically attached to logs and metrics Sentry.setAttribute("user.type", "admin"); Sentry.setAttribute("feature.version", 2); Sentry.setAttribute("debug.enabled", true); From 12d88f20343546c08e536c3a898d2a250e248e32 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 26 Feb 2026 13:36:06 +0100 Subject: [PATCH 10/20] ref: Limit scope attributes sample to base Spring Boot 4 variant Co-Authored-By: Claude Opus 4.6 --- .../samples/spring/boot4/MetricController.java | 2 -- .../samples/spring/boot4/PersonController.java | 4 ---- .../io/sentry/systemtest/MetricsSystemTest.kt | 4 +--- .../io/sentry/systemtest/PersonSystemTest.kt | 15 +-------------- .../samples/spring/boot4/MetricController.java | 2 -- .../samples/spring/boot4/PersonController.java | 4 ---- .../io/sentry/systemtest/MetricsSystemTest.kt | 4 +--- .../io/sentry/systemtest/PersonSystemTest.kt | 15 +-------------- .../samples/spring/boot4/MetricController.java | 2 -- .../samples/spring/boot4/PersonController.java | 4 ---- .../io/sentry/systemtest/MetricsSystemTest.kt | 4 +--- .../io/sentry/systemtest/PersonSystemTest.kt | 15 +-------------- 12 files changed, 6 insertions(+), 69 deletions(-) diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/MetricController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/MetricController.java index be75f5e3002..2a969ec8849 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/MetricController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/MetricController.java @@ -16,8 +16,6 @@ public class MetricController { @GetMapping("count") String count() { - Sentry.setAttribute("user.type", "admin"); - Sentry.setAttribute("feature.version", 2); Sentry.metrics().count("countMetric"); return "count metric increased"; } diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/PersonController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/PersonController.java index c434d67ceb3..b96c840aae8 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/PersonController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/PersonController.java @@ -34,10 +34,6 @@ Person person(@PathVariable Long id) { Sentry.addFeatureFlag("outer-feature-flag", true); Span span = tracer.spanBuilder("spanCreatedThroughOtelApi").startSpan(); try (final @NotNull Scope spanScope = span.makeCurrent()) { - Sentry.setAttribute("user.type", "admin"); - Sentry.setAttribute("feature.version", 2); - Sentry.setAttribute("debug.enabled", true); - Sentry.logger().warn("warn Sentry logging"); Sentry.logger().error("error Sentry logging"); Sentry.logger().info("hello %s %s", "there", "world!"); diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt index 039d9d640c7..dc2ca2a10ae 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -21,9 +21,7 @@ class MetricsSystemTest { assertEquals(200, restClient.lastKnownStatusCode) testHelper.ensureMetricsReceived { event, header -> - testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) && - testHelper.doesMetricHaveAttribute(event, "countMetric", "user.type", "admin") && - testHelper.doesMetricHaveAttribute(event, "countMetric", "feature.version", 2) + testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) } } diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt index 1fe742b64ce..50bc732b657 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -56,20 +56,7 @@ class PersonSystemTest { testHelper.ensureLogsReceived { logs, envelopeHeader -> testHelper.doesContainLogWithBody(logs, "warn Sentry logging") && testHelper.doesContainLogWithBody(logs, "error Sentry logging") && - testHelper.doesContainLogWithBody(logs, "hello there world!") && - testHelper.doesLogWithBodyHaveAttribute( - logs, - "warn Sentry logging", - "user.type", - "admin", - ) && - testHelper.doesLogWithBodyHaveAttribute( - logs, - "warn Sentry logging", - "feature.version", - 2, - ) && - testHelper.doesLogWithBodyHaveAttribute(logs, "warn Sentry logging", "debug.enabled", true) + testHelper.doesContainLogWithBody(logs, "hello there world!") } } diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/MetricController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/MetricController.java index be75f5e3002..2a969ec8849 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/MetricController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/MetricController.java @@ -16,8 +16,6 @@ public class MetricController { @GetMapping("count") String count() { - Sentry.setAttribute("user.type", "admin"); - Sentry.setAttribute("feature.version", 2); Sentry.metrics().count("countMetric"); return "count metric increased"; } diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/PersonController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/PersonController.java index 9b5c9fc3819..bde91c83825 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/PersonController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/PersonController.java @@ -32,10 +32,6 @@ Person person(@PathVariable Long id) { Sentry.addFeatureFlag("transaction-feature-flag", true); Span span = tracer.spanBuilder("spanCreatedThroughOtelApi").startSpan(); try (final @NotNull Scope spanScope = span.makeCurrent()) { - Sentry.setAttribute("user.type", "admin"); - Sentry.setAttribute("feature.version", 2); - Sentry.setAttribute("debug.enabled", true); - Sentry.logger().warn("warn Sentry logging"); Sentry.logger().error("error Sentry logging"); Sentry.logger().info("hello %s %s", "there", "world!"); diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt index 039d9d640c7..dc2ca2a10ae 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -21,9 +21,7 @@ class MetricsSystemTest { assertEquals(200, restClient.lastKnownStatusCode) testHelper.ensureMetricsReceived { event, header -> - testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) && - testHelper.doesMetricHaveAttribute(event, "countMetric", "user.type", "admin") && - testHelper.doesMetricHaveAttribute(event, "countMetric", "feature.version", 2) + testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) } } diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt index ad9b5f77b62..a4d7cc5bdc5 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -51,20 +51,7 @@ class PersonSystemTest { testHelper.ensureLogsReceived { logs, envelopeHeader -> testHelper.doesContainLogWithBody(logs, "warn Sentry logging") && testHelper.doesContainLogWithBody(logs, "error Sentry logging") && - testHelper.doesContainLogWithBody(logs, "hello there world!") && - testHelper.doesLogWithBodyHaveAttribute( - logs, - "warn Sentry logging", - "user.type", - "admin", - ) && - testHelper.doesLogWithBodyHaveAttribute( - logs, - "warn Sentry logging", - "feature.version", - 2, - ) && - testHelper.doesLogWithBodyHaveAttribute(logs, "warn Sentry logging", "debug.enabled", true) + testHelper.doesContainLogWithBody(logs, "hello there world!") } } diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/MetricController.java b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/MetricController.java index be75f5e3002..2a969ec8849 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/MetricController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/MetricController.java @@ -16,8 +16,6 @@ public class MetricController { @GetMapping("count") String count() { - Sentry.setAttribute("user.type", "admin"); - Sentry.setAttribute("feature.version", 2); Sentry.metrics().count("countMetric"); return "count metric increased"; } diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/PersonController.java b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/PersonController.java index e5e00f27846..0db43f5ab71 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/PersonController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/PersonController.java @@ -23,10 +23,6 @@ public PersonController(PersonService personService) { @GetMapping("{id}") Person person(@PathVariable Long id) { - Sentry.setAttribute("user.type", "admin"); - Sentry.setAttribute("feature.version", 2); - Sentry.setAttribute("debug.enabled", true); - Sentry.logger().warn("warn Sentry logging"); Sentry.logger().error("error Sentry logging"); Sentry.logger().info("hello %s %s", "there", "world!"); diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt index 039d9d640c7..dc2ca2a10ae 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -21,9 +21,7 @@ class MetricsSystemTest { assertEquals(200, restClient.lastKnownStatusCode) testHelper.ensureMetricsReceived { event, header -> - testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) && - testHelper.doesMetricHaveAttribute(event, "countMetric", "user.type", "admin") && - testHelper.doesMetricHaveAttribute(event, "countMetric", "feature.version", 2) + testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) } } diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt index 3b7b2751ee9..7ba241200d4 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -35,20 +35,7 @@ class PersonSystemTest { testHelper.ensureLogsReceived { logs, envelopeHeader -> testHelper.doesContainLogWithBody(logs, "warn Sentry logging") && testHelper.doesContainLogWithBody(logs, "error Sentry logging") && - testHelper.doesContainLogWithBody(logs, "hello there world!") && - testHelper.doesLogWithBodyHaveAttribute( - logs, - "warn Sentry logging", - "user.type", - "admin", - ) && - testHelper.doesLogWithBodyHaveAttribute( - logs, - "warn Sentry logging", - "feature.version", - 2, - ) && - testHelper.doesLogWithBodyHaveAttribute(logs, "warn Sentry logging", "debug.enabled", true) + testHelper.doesContainLogWithBody(logs, "hello there world!") } } From bb056cc9e5235a26b3edb6d2be9511845f07e151 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 26 Feb 2026 14:07:21 +0100 Subject: [PATCH 11/20] fix: Detect integer attribute type correctly for all integer Number subtypes Co-Authored-By: Claude Opus 4.6 --- .../java/io/sentry/SentryAttributeType.java | 11 ++- .../java/io/sentry/SentryAttributeTypeTest.kt | 80 +++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 sentry/src/test/java/io/sentry/SentryAttributeTypeTest.kt diff --git a/sentry/src/main/java/io/sentry/SentryAttributeType.java b/sentry/src/main/java/io/sentry/SentryAttributeType.java index 8de00d277d2..86325c248af 100644 --- a/sentry/src/main/java/io/sentry/SentryAttributeType.java +++ b/sentry/src/main/java/io/sentry/SentryAttributeType.java @@ -1,6 +1,9 @@ package io.sentry; +import java.math.BigInteger; import java.util.Locale; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -18,7 +21,13 @@ public enum SentryAttributeType { if (value instanceof Boolean) { return BOOLEAN; } - if (value instanceof Integer) { + if (value instanceof Integer + || value instanceof Long + || value instanceof Short + || value instanceof Byte + || value instanceof BigInteger + || value instanceof AtomicInteger + || value instanceof AtomicLong) { return INTEGER; } if (value instanceof Number) { diff --git a/sentry/src/test/java/io/sentry/SentryAttributeTypeTest.kt b/sentry/src/test/java/io/sentry/SentryAttributeTypeTest.kt new file mode 100644 index 00000000000..f1e1cf169a1 --- /dev/null +++ b/sentry/src/test/java/io/sentry/SentryAttributeTypeTest.kt @@ -0,0 +1,80 @@ +package io.sentry + +import java.math.BigDecimal +import java.math.BigInteger +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicLong +import kotlin.test.Test +import kotlin.test.assertEquals + +class SentryAttributeTypeTest { + + @Test + fun `inferFrom returns BOOLEAN for Boolean`() { + assertEquals(SentryAttributeType.BOOLEAN, SentryAttributeType.inferFrom(true)) + assertEquals(SentryAttributeType.BOOLEAN, SentryAttributeType.inferFrom(false)) + } + + @Test + fun `inferFrom returns INTEGER for Integer`() { + assertEquals(SentryAttributeType.INTEGER, SentryAttributeType.inferFrom(42)) + } + + @Test + fun `inferFrom returns INTEGER for Long`() { + assertEquals(SentryAttributeType.INTEGER, SentryAttributeType.inferFrom(42L)) + } + + @Test + fun `inferFrom returns INTEGER for Short`() { + assertEquals(SentryAttributeType.INTEGER, SentryAttributeType.inferFrom(42.toShort())) + } + + @Test + fun `inferFrom returns INTEGER for Byte`() { + assertEquals(SentryAttributeType.INTEGER, SentryAttributeType.inferFrom(42.toByte())) + } + + @Test + fun `inferFrom returns INTEGER for BigInteger`() { + assertEquals(SentryAttributeType.INTEGER, SentryAttributeType.inferFrom(BigInteger.valueOf(42))) + } + + @Test + fun `inferFrom returns INTEGER for AtomicInteger`() { + assertEquals(SentryAttributeType.INTEGER, SentryAttributeType.inferFrom(AtomicInteger(42))) + } + + @Test + fun `inferFrom returns INTEGER for AtomicLong`() { + assertEquals(SentryAttributeType.INTEGER, SentryAttributeType.inferFrom(AtomicLong(42))) + } + + @Test + fun `inferFrom returns DOUBLE for Double`() { + assertEquals(SentryAttributeType.DOUBLE, SentryAttributeType.inferFrom(3.14)) + } + + @Test + fun `inferFrom returns DOUBLE for Float`() { + assertEquals(SentryAttributeType.DOUBLE, SentryAttributeType.inferFrom(3.14f)) + } + + @Test + fun `inferFrom returns DOUBLE for BigDecimal`() { + assertEquals( + SentryAttributeType.DOUBLE, + SentryAttributeType.inferFrom(BigDecimal.valueOf(3.14)), + ) + } + + @Test + fun `inferFrom returns STRING for String`() { + assertEquals(SentryAttributeType.STRING, SentryAttributeType.inferFrom("hello")) + } + + @Test + fun `inferFrom returns STRING for null`() { + assertEquals(SentryAttributeType.STRING, SentryAttributeType.inferFrom(null)) + } +} From 379187219990d6370a6b1609d1c9084b3cbea8a9 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 26 Feb 2026 14:08:17 +0100 Subject: [PATCH 12/20] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5114beb919e..2c978163593 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixes +- Fix attribute type detection for `Long`, `Short`, `Byte`, `BigInteger`, `AtomicInteger`, and `AtomicLong` being incorrectly inferred as `double` instead of `integer` ([#5122](https://github.com/getsentry/sentry-java/pull/5122)) - Fix crash when unregistering `SystemEventsBroadcastReceiver` with try-catch block. ([#5106](https://github.com/getsentry/sentry-java/pull/5106)) ## 8.33.0 From 858da6c8327b9087847d046e69b391eca19151c4 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 26 Feb 2026 14:41:55 +0100 Subject: [PATCH 13/20] 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 14/20] 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 80755b7e2f7dca0a5f04ff2d2c24941cd62c9ccb Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 26 Feb 2026 15:15:20 +0100 Subject: [PATCH 15/20] use ConcurrentHashMap instead of HashMap when merging attributes --- sentry/src/main/java/io/sentry/CombinedScopeView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry/src/main/java/io/sentry/CombinedScopeView.java b/sentry/src/main/java/io/sentry/CombinedScopeView.java index e04eb722c02..621b718bc04 100644 --- a/sentry/src/main/java/io/sentry/CombinedScopeView.java +++ b/sentry/src/main/java/io/sentry/CombinedScopeView.java @@ -244,7 +244,7 @@ public void removeTag(@Nullable String key) { @Override public @NotNull Map getAttributes() { - final @NotNull Map allAttributes = new HashMap<>(); + final @NotNull Map allAttributes = new ConcurrentHashMap<>(); allAttributes.putAll(globalScope.getAttributes()); allAttributes.putAll(isolationScope.getAttributes()); allAttributes.putAll(scope.getAttributes()); From 5c478cc483b680ab108820f6766ea2eb5cc3a648 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 27 Feb 2026 12:33:53 +0100 Subject: [PATCH 16/20] add enabled check similar to tags --- .../java/io/sentry/CombinedScopeView.java | 1 - sentry/src/main/java/io/sentry/Scopes.java | 38 ++++++++++++++-- sentry/src/test/java/io/sentry/ScopesTest.kt | 44 +++++++++++++++++++ 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/sentry/src/main/java/io/sentry/CombinedScopeView.java b/sentry/src/main/java/io/sentry/CombinedScopeView.java index 621b718bc04..e3869de6ac3 100644 --- a/sentry/src/main/java/io/sentry/CombinedScopeView.java +++ b/sentry/src/main/java/io/sentry/CombinedScopeView.java @@ -14,7 +14,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Queue; diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index 4fbe6040427..0a017a8a915 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -1231,22 +1231,52 @@ public void reportFullyDisplayed() { @Override public void setAttribute(final @Nullable String key, final @Nullable Object value) { - combinedScope.setAttribute(key, value); + if (!isEnabled()) { + getOptions() + .getLogger() + .log( + SentryLevel.WARNING, "Instance is disabled and this 'setAttribute' call is a no-op."); + } else { + getCombinedScopeView().setAttribute(key, value); + } } @Override public void setAttribute(final @NotNull SentryAttribute attribute) { - combinedScope.setAttribute(attribute); + if (!isEnabled()) { + getOptions() + .getLogger() + .log( + SentryLevel.WARNING, "Instance is disabled and this 'setAttribute' call is a no-op."); + } else { + getCombinedScopeView().setAttribute(attribute); + } } @Override public void setAttributes(final @NotNull SentryAttributes attributes) { - combinedScope.setAttributes(attributes); + if (!isEnabled()) { + getOptions() + .getLogger() + .log( + SentryLevel.WARNING, + "Instance is disabled and this 'setAttributes' call is a no-op."); + } else { + getCombinedScopeView().setAttributes(attributes); + } } @Override public void removeAttribute(final @Nullable String key) { - combinedScope.removeAttribute(key); + if (!isEnabled()) { + getOptions() + .getLogger() + .log( + SentryLevel.WARNING, + "Instance is disabled and this 'removeAttribute' call is a no-op."); + } else { + getCombinedScopeView().removeAttribute(key); + } } @Override diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index 73a14b38e71..a8c82e81df8 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -1290,6 +1290,50 @@ class ScopesTest { // endregion + // region setAttribute tests + @Test + fun `when setAttribute is called on disabled client, do nothing`() { + val scopes = generateScopes() + var scope: IScope? = null + scopes.configureScope { scope = it } + scopes.close() + + scopes.setAttribute("test", "test") + assertEquals(0, scope?.attributes?.count()) + } + + @Test + fun `when setAttribute with SentryAttribute is called on disabled client, do nothing`() { + val scopes = generateScopes() + var scope: IScope? = null + scopes.configureScope { scope = it } + scopes.close() + + scopes.setAttribute(SentryAttribute.stringAttribute("test", "test")) + assertEquals(0, scope?.attributes?.count()) + } + + @Test + fun `when setAttributes is called on disabled client, do nothing`() { + val scopes = generateScopes() + var scope: IScope? = null + scopes.configureScope { scope = it } + scopes.close() + + scopes.setAttributes(SentryAttributes.of(SentryAttribute.stringAttribute("test", "test"))) + assertEquals(0, scope?.attributes?.count()) + } + + @Test + fun `when removeAttribute is called on disabled client, do nothing`() { + val scopes = generateScopes() + scopes.close() + + scopes.removeAttribute("test") + } + + // endregion + // region captureEnvelope tests @Test fun `when captureEnvelope is called and envelope is null, throws IllegalArgumentException`() { From 7c750cf125362e9ac82c58d1168d90342433dc4f Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 27 Feb 2026 12:53:07 +0100 Subject: [PATCH 17/20] make setAttribute and setAttributes params nullable --- sentry/src/main/java/io/sentry/CombinedScopeView.java | 4 ++-- sentry/src/main/java/io/sentry/HubAdapter.java | 4 ++-- sentry/src/main/java/io/sentry/HubScopesWrapper.java | 4 ++-- sentry/src/main/java/io/sentry/IScope.java | 4 ++-- sentry/src/main/java/io/sentry/IScopes.java | 4 ++-- sentry/src/main/java/io/sentry/NoOpHub.java | 4 ++-- sentry/src/main/java/io/sentry/NoOpScope.java | 4 ++-- sentry/src/main/java/io/sentry/NoOpScopes.java | 4 ++-- sentry/src/main/java/io/sentry/Scope.java | 4 ++-- sentry/src/main/java/io/sentry/Scopes.java | 4 ++-- sentry/src/main/java/io/sentry/ScopesAdapter.java | 4 ++-- sentry/src/main/java/io/sentry/Sentry.java | 4 ++-- 12 files changed, 24 insertions(+), 24 deletions(-) diff --git a/sentry/src/main/java/io/sentry/CombinedScopeView.java b/sentry/src/main/java/io/sentry/CombinedScopeView.java index e3869de6ac3..0c61bdf9126 100644 --- a/sentry/src/main/java/io/sentry/CombinedScopeView.java +++ b/sentry/src/main/java/io/sentry/CombinedScopeView.java @@ -256,12 +256,12 @@ public void setAttribute(@Nullable String key, @Nullable Object value) { } @Override - public void setAttribute(@NotNull SentryAttribute attribute) { + public void setAttribute(@Nullable SentryAttribute attribute) { getDefaultWriteScope().setAttribute(attribute); } @Override - public void setAttributes(@NotNull SentryAttributes attributes) { + public void setAttributes(@Nullable SentryAttributes attributes) { getDefaultWriteScope().setAttributes(attributes); } diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index 4fc6979818d..cf90eb1fe65 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -401,12 +401,12 @@ public void setAttribute(final @Nullable String key, final @Nullable Object valu } @Override - public void setAttribute(final @NotNull SentryAttribute attribute) { + public void setAttribute(final @Nullable SentryAttribute attribute) { Sentry.setAttribute(attribute); } @Override - public void setAttributes(final @NotNull SentryAttributes attributes) { + public void setAttributes(final @Nullable SentryAttributes attributes) { Sentry.setAttributes(attributes); } diff --git a/sentry/src/main/java/io/sentry/HubScopesWrapper.java b/sentry/src/main/java/io/sentry/HubScopesWrapper.java index dffd0926bf7..66a34b4dc36 100644 --- a/sentry/src/main/java/io/sentry/HubScopesWrapper.java +++ b/sentry/src/main/java/io/sentry/HubScopesWrapper.java @@ -386,12 +386,12 @@ public void setAttribute(final @Nullable String key, final @Nullable Object valu } @Override - public void setAttribute(final @NotNull SentryAttribute attribute) { + public void setAttribute(final @Nullable SentryAttribute attribute) { scopes.setAttribute(attribute); } @Override - public void setAttributes(final @NotNull SentryAttributes attributes) { + public void setAttributes(final @Nullable SentryAttributes attributes) { scopes.setAttributes(attributes); } diff --git a/sentry/src/main/java/io/sentry/IScope.java b/sentry/src/main/java/io/sentry/IScope.java index 1d7763037ac..ccab8dbdeb3 100644 --- a/sentry/src/main/java/io/sentry/IScope.java +++ b/sentry/src/main/java/io/sentry/IScope.java @@ -438,14 +438,14 @@ void setSpanContext( * * @param attribute the attribute */ - void setAttribute(final @NotNull SentryAttribute attribute); + void setAttribute(final @Nullable SentryAttribute attribute); /** * Sets multiple attributes on the Scope. * * @param attributes the attributes */ - void setAttributes(final @NotNull SentryAttributes attributes); + void setAttributes(final @Nullable SentryAttributes attributes); /** * Removes an attribute from the Scope. diff --git a/sentry/src/main/java/io/sentry/IScopes.java b/sentry/src/main/java/io/sentry/IScopes.java index db7313e5594..b1b437f72e5 100644 --- a/sentry/src/main/java/io/sentry/IScopes.java +++ b/sentry/src/main/java/io/sentry/IScopes.java @@ -761,14 +761,14 @@ default boolean isNoOp() { * * @param attribute the attribute */ - void setAttribute(final @NotNull SentryAttribute attribute); + void setAttribute(final @Nullable SentryAttribute attribute); /** * Sets multiple attributes. * * @param attributes the attributes */ - void setAttributes(final @NotNull SentryAttributes attributes); + void setAttributes(final @Nullable SentryAttributes attributes); /** * Removes an attribute. diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index 39b43745d44..4a02be1bd40 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -342,10 +342,10 @@ public boolean isNoOp() { public void setAttribute(final @Nullable String key, final @Nullable Object value) {} @Override - public void setAttribute(final @NotNull SentryAttribute attribute) {} + public void setAttribute(final @Nullable SentryAttribute attribute) {} @Override - public void setAttributes(final @NotNull SentryAttributes attributes) {} + public void setAttributes(final @Nullable SentryAttributes attributes) {} @Override public void removeAttribute(final @Nullable String key) {} diff --git a/sentry/src/main/java/io/sentry/NoOpScope.java b/sentry/src/main/java/io/sentry/NoOpScope.java index 34f68bc78cb..7693ab81deb 100644 --- a/sentry/src/main/java/io/sentry/NoOpScope.java +++ b/sentry/src/main/java/io/sentry/NoOpScope.java @@ -304,10 +304,10 @@ public void replaceOptions(@NotNull SentryOptions options) {} public void setAttribute(@Nullable String key, @Nullable Object value) {} @Override - public void setAttribute(@NotNull SentryAttribute attribute) {} + public void setAttribute(@Nullable SentryAttribute attribute) {} @Override - public void setAttributes(@NotNull SentryAttributes attributes) {} + public void setAttributes(@Nullable SentryAttributes attributes) {} @Override public void removeAttribute(@Nullable String key) {} diff --git a/sentry/src/main/java/io/sentry/NoOpScopes.java b/sentry/src/main/java/io/sentry/NoOpScopes.java index a03a7582b23..1ae357d502e 100644 --- a/sentry/src/main/java/io/sentry/NoOpScopes.java +++ b/sentry/src/main/java/io/sentry/NoOpScopes.java @@ -340,10 +340,10 @@ public boolean isNoOp() { public void setAttribute(final @Nullable String key, final @Nullable Object value) {} @Override - public void setAttribute(final @NotNull SentryAttribute attribute) {} + public void setAttribute(final @Nullable SentryAttribute attribute) {} @Override - public void setAttributes(final @NotNull SentryAttributes attributes) {} + public void setAttributes(final @Nullable SentryAttributes attributes) {} @Override public void removeAttribute(final @Nullable String key) {} diff --git a/sentry/src/main/java/io/sentry/Scope.java b/sentry/src/main/java/io/sentry/Scope.java index 7c926b68df3..1aab545b80c 100644 --- a/sentry/src/main/java/io/sentry/Scope.java +++ b/sentry/src/main/java/io/sentry/Scope.java @@ -656,7 +656,7 @@ public void setAttribute(final @Nullable String key, final @Nullable Object valu /** {@inheritDoc} */ @Override - public void setAttribute(final @NotNull SentryAttribute attribute) { + public void setAttribute(final @Nullable SentryAttribute attribute) { if (attribute == null) { return; } @@ -665,7 +665,7 @@ public void setAttribute(final @NotNull SentryAttribute attribute) { /** {@inheritDoc} */ @Override - public void setAttributes(final @NotNull SentryAttributes attributes) { + public void setAttributes(final @Nullable SentryAttributes attributes) { if (attributes == null) { return; } diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index 0a017a8a915..a8ae0359d1e 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -1242,7 +1242,7 @@ public void setAttribute(final @Nullable String key, final @Nullable Object valu } @Override - public void setAttribute(final @NotNull SentryAttribute attribute) { + public void setAttribute(final @Nullable SentryAttribute attribute) { if (!isEnabled()) { getOptions() .getLogger() @@ -1254,7 +1254,7 @@ public void setAttribute(final @NotNull SentryAttribute attribute) { } @Override - public void setAttributes(final @NotNull SentryAttributes attributes) { + public void setAttributes(final @Nullable SentryAttributes attributes) { if (!isEnabled()) { getOptions() .getLogger() diff --git a/sentry/src/main/java/io/sentry/ScopesAdapter.java b/sentry/src/main/java/io/sentry/ScopesAdapter.java index 4487a96f22f..b66b681a332 100644 --- a/sentry/src/main/java/io/sentry/ScopesAdapter.java +++ b/sentry/src/main/java/io/sentry/ScopesAdapter.java @@ -398,12 +398,12 @@ public void setAttribute(final @Nullable String key, final @Nullable Object valu } @Override - public void setAttribute(final @NotNull SentryAttribute attribute) { + public void setAttribute(final @Nullable SentryAttribute attribute) { Sentry.setAttribute(attribute); } @Override - public void setAttributes(final @NotNull SentryAttributes attributes) { + public void setAttributes(final @Nullable SentryAttributes attributes) { Sentry.setAttributes(attributes); } diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index d50150efa50..500a6e7f16d 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -1383,7 +1383,7 @@ public static void setAttribute(final @Nullable String key, final @Nullable Obje * * @param attribute the attribute */ - public static void setAttribute(final @NotNull SentryAttribute attribute) { + public static void setAttribute(final @Nullable SentryAttribute attribute) { getCurrentScopes().setAttribute(attribute); } @@ -1392,7 +1392,7 @@ public static void setAttribute(final @NotNull SentryAttribute attribute) { * * @param attributes the attributes */ - public static void setAttributes(final @NotNull SentryAttributes attributes) { + public static void setAttributes(final @Nullable SentryAttributes attributes) { getCurrentScopes().setAttributes(attributes); } From 8e61d4dfb0c7c8147041652572b68636cd06db02 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 27 Feb 2026 13:44:26 +0100 Subject: [PATCH 18/20] 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(), From 94afb06fe8fb9ccce279a6b48b42a442917b50bc Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 3 Mar 2026 17:38:27 +0100 Subject: [PATCH 19/20] feat: Add Object[] overload to arrayAttribute factory The arrayAttribute() factory only accepted Collection, but inferFrom() also handles native Java arrays. Add an Object[] overload so users can pass object arrays like String[] directly without falling back to the untyped named() method. Co-Authored-By: Claude --- sentry/api/sentry.api | 1 + sentry/src/main/java/io/sentry/SentryAttribute.java | 5 +++++ sentry/src/test/java/io/sentry/SentryAttributeTypeTest.kt | 7 +++++++ 3 files changed, 13 insertions(+) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 060ebe2ed9c..73495faab35 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2814,6 +2814,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 arrayAttribute (Ljava/lang/String;[Ljava/lang/Object;)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; diff --git a/sentry/src/main/java/io/sentry/SentryAttribute.java b/sentry/src/main/java/io/sentry/SentryAttribute.java index 5064eeedf67..213ef53d91e 100644 --- a/sentry/src/main/java/io/sentry/SentryAttribute.java +++ b/sentry/src/main/java/io/sentry/SentryAttribute.java @@ -60,4 +60,9 @@ private SentryAttribute( final @NotNull String name, final @Nullable Collection value) { return new SentryAttribute(name, SentryAttributeType.ARRAY, value); } + + public static @NotNull SentryAttribute arrayAttribute( + final @NotNull String name, final @Nullable Object[] value) { + return new SentryAttribute(name, SentryAttributeType.ARRAY, value); + } } diff --git a/sentry/src/test/java/io/sentry/SentryAttributeTypeTest.kt b/sentry/src/test/java/io/sentry/SentryAttributeTypeTest.kt index 9516d89b7e0..d8d3a35db3b 100644 --- a/sentry/src/test/java/io/sentry/SentryAttributeTypeTest.kt +++ b/sentry/src/test/java/io/sentry/SentryAttributeTypeTest.kt @@ -112,4 +112,11 @@ class SentryAttributeTypeTest { fun `inferFrom returns ARRAY for mixed-type list`() { assertEquals(SentryAttributeType.ARRAY, SentryAttributeType.inferFrom(listOf("a", 1, true))) } + + @Test + fun `arrayAttribute factory accepts Object array`() { + val attr = SentryAttribute.arrayAttribute("key", arrayOf("a", "b")) + assertEquals("key", attr.name) + assertEquals(SentryAttributeType.ARRAY, attr.type) + } } From 1c603b013a7be9d8b702018ac974f33e316893f0 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 4 Mar 2026 06:35:17 +0100 Subject: [PATCH 20/20] shape changelog --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3ba5fdf853..5897a0831d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,10 @@ ### 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)) +- Add scope-level attributes API ([#5118](https://github.com/getsentry/sentry-java/pull/5118)) via ([#5148](https://github.com/getsentry/sentry-java/pull/5148)) + - Automatically include scope attributes in logs and metrics ([#5120](https://github.com/getsentry/sentry-java/pull/5120)) + - New APIs are `Sentry.setAttribute`, `Sentry.setAttributes`, `Sentry.removeAttribute` +- Support collections and arrays in attribute type inference ([#5124](https://github.com/getsentry/sentry-java/pull/5124)) - Create `sentry-opentelemetry-otlp` and `sentry-opentelemetry-otlp-spring` modules for combining OpenTelemetry SDK OTLP export with Sentry SDK ([#5100](https://github.com/getsentry/sentry-java/pull/5100)) - OpenTelemetry is configured to send spans to Sentry directly using an OTLP endpoint. - Sentry only uses trace and span ID from OpenTelemetry (via `OpenTelemetryOtlpEventProcessor`) but will not send spans through OpenTelemetry nor use OpenTelemetry `Context` for `Scopes` propagation.