Skip to content

Commit 4b1510b

Browse files
adinauerclaude
andauthored
feat(core): Global Attributes API (#5148)
* 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 <noreply@anthropic.com> * changelog * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * changelog * 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 <noreply@anthropic.com> * changelog * Revert "changelog" This reverts commit 7189bdc. * ref: Remove redundant comments from variant controllers Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ref: Limit scope attributes sample to base Spring Boot 4 variant Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: Detect integer attribute type correctly for all integer Number subtypes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * changelog * feat: Support collections and arrays in log attribute type inference Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * changelog * use ConcurrentHashMap instead of HashMap when merging attributes * add enabled check similar to tags * make setAttribute and setAttributes params nullable * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * shape changelog --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 815e034 commit 4b1510b

File tree

31 files changed

+963
-38
lines changed

31 files changed

+963
-38
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
### Features
66

7+
- Add scope-level attributes API ([#5118](https://github.com/getsentry/sentry-java/pull/5118)) via ([#5148](https://github.com/getsentry/sentry-java/pull/5148))
8+
- Automatically include scope attributes in logs and metrics ([#5120](https://github.com/getsentry/sentry-java/pull/5120))
9+
- New APIs are `Sentry.setAttribute`, `Sentry.setAttributes`, `Sentry.removeAttribute`
10+
- Support collections and arrays in attribute type inference ([#5124](https://github.com/getsentry/sentry-java/pull/5124))
711
- Add support for `SENTRY_SAMPLE_RATE` environment variable / `sample-rate` property ([#5112](https://github.com/getsentry/sentry-java/pull/5112))
812
- 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))
913
- OpenTelemetry is configured to send spans to Sentry directly using an OTLP endpoint.
@@ -32,6 +36,7 @@
3236

3337
### Fixes
3438

39+
- 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))
3540
- Remove `AndroidRuntimeManager` StrictMode relaxation to prevent ANRs during SDK init ([#5127](https://github.com/getsentry/sentry-java/pull/5127))
3641
- **IMPORTANT:** StrictMode violations may appear again in debug builds. This is intentional to prevent ANRs in production releases.
3742
- Fix crash when unregistering `SystemEventsBroadcastReceiver` with try-catch block. ([#5106](https://github.com/getsentry/sentry-java/pull/5106))

sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/MetricController.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ public class MetricController {
1616

1717
@GetMapping("count")
1818
String count() {
19+
Sentry.setAttribute("user.type", "admin");
20+
Sentry.setAttribute("feature.version", 2);
1921
Sentry.metrics().count("countMetric");
2022
return "count metric increased";
2123
}

sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/PersonController.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ Person person(@PathVariable Long id) {
2727
ISpan currentSpan = Sentry.getSpan();
2828
ISpan sentrySpan = currentSpan.startChild("spanCreatedThroughSentryApi");
2929
try {
30+
Sentry.setAttribute("user.type", "admin");
31+
Sentry.setAttribute("feature.version", 2);
32+
Sentry.setAttribute("debug.enabled", true);
33+
3034
Sentry.logger().warn("warn Sentry logging");
3135
Sentry.logger().error("error Sentry logging");
3236
Sentry.logger().info("hello %s %s", "there", "world!");

sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ class MetricsSystemTest {
2121
assertEquals(200, restClient.lastKnownStatusCode)
2222

2323
testHelper.ensureMetricsReceived { event, header ->
24-
testHelper.doesContainMetric(event, "countMetric", "counter", 1.0)
24+
testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) &&
25+
testHelper.doesMetricHaveAttribute(event, "countMetric", "user.type", "admin") &&
26+
testHelper.doesMetricHaveAttribute(event, "countMetric", "feature.version", 2)
2527
}
2628
}
2729

sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,20 @@ class PersonSystemTest {
5050
testHelper.ensureLogsReceived { logs, envelopeHeader ->
5151
testHelper.doesContainLogWithBody(logs, "warn Sentry logging") &&
5252
testHelper.doesContainLogWithBody(logs, "error Sentry logging") &&
53-
testHelper.doesContainLogWithBody(logs, "hello there world!")
53+
testHelper.doesContainLogWithBody(logs, "hello there world!") &&
54+
testHelper.doesLogWithBodyHaveAttribute(
55+
logs,
56+
"warn Sentry logging",
57+
"user.type",
58+
"admin",
59+
) &&
60+
testHelper.doesLogWithBodyHaveAttribute(
61+
logs,
62+
"warn Sentry logging",
63+
"feature.version",
64+
2,
65+
) &&
66+
testHelper.doesLogWithBodyHaveAttribute(logs, "warn Sentry logging", "debug.enabled", true)
5467
}
5568
}
5669

sentry-system-test-support/api/sentry-system-test-support.api

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,8 @@ public final class io/sentry/systemtest/util/TestHelper {
574574
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
575575
public final fun doesEventHaveExceptionMessage (Lio/sentry/SentryEvent;Ljava/lang/String;)Z
576576
public final fun doesEventHaveFlag (Lio/sentry/SentryEvent;Ljava/lang/String;Z)Z
577+
public final fun doesLogWithBodyHaveAttribute (Lio/sentry/SentryLogEvents;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)Z
578+
public final fun doesMetricHaveAttribute (Lio/sentry/SentryMetricsEvents;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)Z
577579
public final fun doesTransactionContainSpanWithDescription (Lio/sentry/protocol/SentryTransaction;Ljava/lang/String;)Z
578580
public final fun doesTransactionContainSpanWithOp (Lio/sentry/protocol/SentryTransaction;Ljava/lang/String;)Z
579581
public final fun doesTransactionContainSpanWithOpAndDescription (Lio/sentry/protocol/SentryTransaction;Ljava/lang/String;Ljava/lang/String;)Z

sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/TestHelper.kt

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,68 @@ class TestHelper(backendUrl: String) {
190190
return true
191191
}
192192

193+
fun doesLogWithBodyHaveAttribute(
194+
logs: SentryLogEvents,
195+
body: String,
196+
attributeKey: String,
197+
attributeValue: Any?,
198+
): Boolean {
199+
val logItem = logs.items.firstOrNull { logItem -> logItem.body == body }
200+
if (logItem == null) {
201+
println("Unable to find log item with body $body in logs:")
202+
logObject(logs)
203+
return false
204+
}
205+
206+
val attr = logItem.attributes?.get(attributeKey)
207+
if (attr == null) {
208+
println("Unable to find attribute $attributeKey on log with body $body:")
209+
logObject(logItem)
210+
return false
211+
}
212+
213+
if (attr.value != attributeValue) {
214+
println(
215+
"Attribute $attributeKey has value ${attr.value} but expected $attributeValue on log with body $body:"
216+
)
217+
logObject(logItem)
218+
return false
219+
}
220+
221+
return true
222+
}
223+
224+
fun doesMetricHaveAttribute(
225+
metrics: SentryMetricsEvents,
226+
metricName: String,
227+
attributeKey: String,
228+
attributeValue: Any?,
229+
): Boolean {
230+
val metricItem = metrics.items.firstOrNull { it.name == metricName }
231+
if (metricItem == null) {
232+
println("Unable to find metric with name $metricName in metrics:")
233+
logObject(metrics)
234+
return false
235+
}
236+
237+
val attr = metricItem.attributes?.get(attributeKey)
238+
if (attr == null) {
239+
println("Unable to find attribute $attributeKey on metric $metricName:")
240+
logObject(metricItem)
241+
return false
242+
}
243+
244+
if (attr.value != attributeValue) {
245+
println(
246+
"Attribute $attributeKey has value ${attr.value} but expected $attributeValue on metric $metricName:"
247+
)
248+
logObject(metricItem)
249+
return false
250+
}
251+
252+
return true
253+
}
254+
193255
private fun checkIfTransactionMatches(
194256
envelopeString: String,
195257
callback: ((SentryTransaction, SentryEnvelopeHeader) -> Boolean),

0 commit comments

Comments
 (0)