From e156d3ca8d99b8f18bee49f49d7e1a055086162b Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 17 Dec 2025 12:44:31 +0100 Subject: [PATCH 01/40] Metrics Options --- sentry/api/sentry.api | 18 +++- .../main/java/io/sentry/SentryOptions.java | 87 ++++++++++++++++--- 2 files changed, 87 insertions(+), 18 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index f6729869eb..b72982774d 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -3420,6 +3420,7 @@ public class io/sentry/SentryOptions { public fun getMaxRequestBodySize ()Lio/sentry/SentryOptions$RequestSize; public fun getMaxSpans ()I public fun getMaxTraceFileSize ()J + public fun getMetrics ()Lio/sentry/SentryOptions$Metrics; public fun getModulesLoader ()Lio/sentry/internal/modules/IModulesLoader; public fun getOnDiscard ()Lio/sentry/SentryOptions$OnDiscardCallback; public fun getOnOversizedEvent ()Lio/sentry/SentryOptions$OnOversizedEventCallback; @@ -3572,6 +3573,7 @@ public class io/sentry/SentryOptions { public fun setMaxRequestBodySize (Lio/sentry/SentryOptions$RequestSize;)V public fun setMaxSpans (I)V public fun setMaxTraceFileSize (J)V + public fun setMetrics (Lio/sentry/SentryOptions$Metrics;)V public fun setModulesLoader (Lio/sentry/internal/modules/IModulesLoader;)V public fun setOnDiscard (Lio/sentry/SentryOptions$OnDiscardCallback;)V public fun setOnOversizedEvent (Lio/sentry/SentryOptions$OnOversizedEventCallback;)V @@ -3626,10 +3628,6 @@ public abstract interface class io/sentry/SentryOptions$BeforeBreadcrumbCallback public abstract fun execute (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)Lio/sentry/Breadcrumb; } -public abstract interface class io/sentry/SentryOptions$BeforeEmitMetricCallback { - public abstract fun execute (Ljava/lang/String;Ljava/util/Map;)Z -} - public abstract interface class io/sentry/SentryOptions$BeforeEnvelopeCallback { public abstract fun execute (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V } @@ -3683,6 +3681,18 @@ public abstract interface class io/sentry/SentryOptions$Logs$BeforeSendLogCallba public abstract fun execute (Lio/sentry/SentryLogEvent;)Lio/sentry/SentryLogEvent; } +public final class io/sentry/SentryOptions$Metrics { + public fun ()V + public fun getBeforeSend ()Lio/sentry/SentryOptions$Metrics$BeforeSendMetricCallback; + public fun isEnabled ()Z + public fun setBeforeSend (Lio/sentry/SentryOptions$Metrics$BeforeSendMetricCallback;)V + public fun setEnabled (Z)V +} + +public abstract interface class io/sentry/SentryOptions$Metrics$BeforeSendMetricCallback { + public abstract fun execute (Ljava/lang/Object;)Ljava/lang/Object; +} + public abstract interface class io/sentry/SentryOptions$OnDiscardCallback { public abstract fun execute (Lio/sentry/clientreport/DiscardReason;Lio/sentry/DataCategory;Ljava/lang/Long;)V } diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 835411cb61..2575328c70 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -624,6 +624,8 @@ public class SentryOptions { private @NotNull SentryOptions.Logs logs = new SentryOptions.Logs(); + private @NotNull SentryOptions.Metrics metrics = new SentryOptions.Metrics(); + private @NotNull ISocketTagger socketTagger = NoOpSocketTagger.getInstance(); /** Runtime manager to manage runtime policies, like StrictMode on Android. */ @@ -3261,20 +3263,6 @@ public interface BeforeEnvelopeCallback { void execute(@NotNull SentryEnvelope envelope, @Nullable Hint hint); } - /** The BeforeEmitMetric callback */ - @ApiStatus.Experimental - public interface BeforeEmitMetricCallback { - - /** - * A callback which gets called right before a metric is about to be emitted. - * - * @param key the metric key - * @param tags the metric tags - * @return true if the metric should be emitted, false otherwise - */ - boolean execute(@NotNull String key, @Nullable Map tags); - } - /** * Creates SentryOptions instance without initializing any of the internal parts. * @@ -3537,6 +3525,16 @@ public void setLogs(@NotNull SentryOptions.Logs logs) { this.logs = logs; } + @ApiStatus.Experimental + public @NotNull SentryOptions.Metrics getMetrics() { + return metrics; + } + + @ApiStatus.Experimental + public void setMetrics(@NotNull SentryOptions.Metrics metrics) { + this.metrics = metrics; + } + public static final class Proxy { private @Nullable String host; private @Nullable String port; @@ -3741,6 +3739,67 @@ public interface BeforeSendLogCallback { } } + public static final class Metrics { + + /** Whether Sentry Metrics feature is enabled and metrics are sent to Sentry. */ + private boolean enable = false; + + /** + * This function is called with a metric key and tags and can return false to skip sending the + * metric + */ + private @Nullable BeforeSendMetricCallback beforeSend; + + /** + * Whether Sentry Metrics feature is enabled and metrics are sent to Sentry. + * + * @return true if Sentry Metrics should be enabled + */ + public boolean isEnabled() { + return enable; + } + + /** + * Whether Sentry Metrics feature is enabled and metrics are sent to Sentry. + * + * @param enableMetrics true if Sentry Metrics should be enabled + */ + public void setEnabled(boolean enableMetrics) { + this.enable = enableMetrics; + } + + /** + * Returns the BeforeSendMetric callback + * + * @return the beforeSend callback or null if not set + */ + public @Nullable BeforeSendMetricCallback getBeforeSend() { + return beforeSend; + } + + /** + * Sets the beforeSend callback for metrics + * + * @param beforeSend the beforeSend callback for metrics + */ + public void setBeforeSend(@Nullable BeforeSendMetricCallback beforeSend) { + this.beforeSend = beforeSend; + } + + public interface BeforeSendMetricCallback { + + /** + * A callback which gets called right before a metric is about to be sent. + * + * @param metric the metric + * @return the original metric, mutated metric or null if metric was dropped + */ + // TODO replace with SentryMetric + @Nullable + Object execute(@NotNull Object metric); + } + } + @ApiStatus.Experimental public @NotNull DistributionOptions getDistribution() { return distribution; From b0ec8ebc20a0f8b97426b89415e1d0e97570bf85 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 17 Dec 2025 14:51:32 +0100 Subject: [PATCH 02/40] MetricsApi stub --- sentry/api/sentry.api | 22 +++++++++++++++++++ .../src/main/java/io/sentry/HubAdapter.java | 6 +++++ .../main/java/io/sentry/HubScopesWrapper.java | 6 +++++ sentry/src/main/java/io/sentry/IScopes.java | 4 ++++ sentry/src/main/java/io/sentry/NoOpHub.java | 7 ++++++ .../src/main/java/io/sentry/NoOpScopes.java | 7 ++++++ sentry/src/main/java/io/sentry/Scopes.java | 9 ++++++++ .../main/java/io/sentry/ScopesAdapter.java | 6 +++++ sentry/src/main/java/io/sentry/Sentry.java | 6 +++++ .../java/io/sentry/metrics/IMetricsApi.java | 11 ++++++++++ .../java/io/sentry/metrics/MetricsApi.java | 18 +++++++++++++++ .../io/sentry/metrics/NoOpMetricsApi.java | 16 ++++++++++++++ 12 files changed, 118 insertions(+) create mode 100644 sentry/src/main/java/io/sentry/metrics/IMetricsApi.java create mode 100644 sentry/src/main/java/io/sentry/metrics/MetricsApi.java create mode 100644 sentry/src/main/java/io/sentry/metrics/NoOpMetricsApi.java diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index b72982774d..f947368b75 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -661,6 +661,7 @@ public final class io/sentry/HubAdapter : io/sentry/IHub { public fun isHealthy ()Z public fun logger ()Lio/sentry/logger/ILoggerApi; public fun makeCurrent ()Lio/sentry/ISentryLifecycleToken; + public fun metrics ()Lio/sentry/metrics/IMetricsApi; public fun popScope ()V public fun pushIsolationScope ()Lio/sentry/ISentryLifecycleToken; public fun pushScope ()Lio/sentry/ISentryLifecycleToken; @@ -732,6 +733,7 @@ public final class io/sentry/HubScopesWrapper : io/sentry/IHub { public fun isHealthy ()Z public fun logger ()Lio/sentry/logger/ILoggerApi; public fun makeCurrent ()Lio/sentry/ISentryLifecycleToken; + public fun metrics ()Lio/sentry/metrics/IMetricsApi; public fun popScope ()V public fun pushIsolationScope ()Lio/sentry/ISentryLifecycleToken; public fun pushScope ()Lio/sentry/ISentryLifecycleToken; @@ -992,6 +994,7 @@ public abstract interface class io/sentry/IScopes { public fun isNoOp ()Z public abstract fun logger ()Lio/sentry/logger/ILoggerApi; public abstract fun makeCurrent ()Lio/sentry/ISentryLifecycleToken; + public abstract fun metrics ()Lio/sentry/metrics/IMetricsApi; public abstract fun popScope ()V public abstract fun pushIsolationScope ()Lio/sentry/ISentryLifecycleToken; public abstract fun pushScope ()Lio/sentry/ISentryLifecycleToken; @@ -1565,6 +1568,7 @@ public final class io/sentry/NoOpHub : io/sentry/IHub { public fun isNoOp ()Z public fun logger ()Lio/sentry/logger/ILoggerApi; public fun makeCurrent ()Lio/sentry/ISentryLifecycleToken; + public fun metrics ()Lio/sentry/metrics/IMetricsApi; public fun popScope ()V public fun pushIsolationScope ()Lio/sentry/ISentryLifecycleToken; public fun pushScope ()Lio/sentry/ISentryLifecycleToken; @@ -1741,6 +1745,7 @@ public final class io/sentry/NoOpScopes : io/sentry/IScopes { public fun isNoOp ()Z public fun logger ()Lio/sentry/logger/ILoggerApi; public fun makeCurrent ()Lio/sentry/ISentryLifecycleToken; + public fun metrics ()Lio/sentry/metrics/IMetricsApi; public fun popScope ()V public fun pushIsolationScope ()Lio/sentry/ISentryLifecycleToken; public fun pushScope ()Lio/sentry/ISentryLifecycleToken; @@ -2456,6 +2461,7 @@ public final class io/sentry/Scopes : io/sentry/IScopes { public fun isHealthy ()Z public fun logger ()Lio/sentry/logger/ILoggerApi; public fun makeCurrent ()Lio/sentry/ISentryLifecycleToken; + public fun metrics ()Lio/sentry/metrics/IMetricsApi; public fun popScope ()V public fun pushIsolationScope ()Lio/sentry/ISentryLifecycleToken; public fun pushScope ()Lio/sentry/ISentryLifecycleToken; @@ -2528,6 +2534,7 @@ public final class io/sentry/ScopesAdapter : io/sentry/IScopes { public fun isHealthy ()Z public fun logger ()Lio/sentry/logger/ILoggerApi; public fun makeCurrent ()Lio/sentry/ISentryLifecycleToken; + public fun metrics ()Lio/sentry/metrics/IMetricsApi; public fun popScope ()V public fun pushIsolationScope ()Lio/sentry/ISentryLifecycleToken; public fun pushScope ()Lio/sentry/ISentryLifecycleToken; @@ -2648,6 +2655,7 @@ public final class io/sentry/Sentry { public static fun isEnabled ()Z public static fun isHealthy ()Z public static fun logger ()Lio/sentry/logger/ILoggerApi; + public static fun metrics ()Lio/sentry/metrics/IMetricsApi; public static fun popScope ()V public static fun pushIsolationScope ()Lio/sentry/ISentryLifecycleToken; public static fun pushScope ()Lio/sentry/ISentryLifecycleToken; @@ -5123,6 +5131,20 @@ public final class io/sentry/logger/SentryLogParameters { public fun setTimestamp (Lio/sentry/SentryDate;)V } +public abstract interface class io/sentry/metrics/IMetricsApi { + public abstract fun count (Ljava/lang/String;)V +} + +public final class io/sentry/metrics/MetricsApi : io/sentry/metrics/IMetricsApi { + public fun (Lio/sentry/Scopes;)V + public fun count (Ljava/lang/String;)V +} + +public final class io/sentry/metrics/NoOpMetricsApi : io/sentry/metrics/IMetricsApi { + public fun count (Ljava/lang/String;)V + public static fun getInstance ()Lio/sentry/metrics/NoOpMetricsApi; +} + public final class io/sentry/opentelemetry/OpenTelemetryUtil { public fun ()V public static fun applyIgnoredSpanOrigins (Lio/sentry/SentryOptions;)V diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index 31a2e219cd..5715d06114 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -1,6 +1,7 @@ package io.sentry; import io.sentry.logger.ILoggerApi; +import io.sentry.metrics.IMetricsApi; import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; @@ -389,6 +390,11 @@ public void reportFullyDisplayed() { return Sentry.getCurrentScopes().logger(); } + @Override + public @NotNull IMetricsApi metrics() { + return Sentry.getCurrentScopes().metrics(); + } + @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 d15ed72ee4..c04aad9ed8 100644 --- a/sentry/src/main/java/io/sentry/HubScopesWrapper.java +++ b/sentry/src/main/java/io/sentry/HubScopesWrapper.java @@ -1,6 +1,7 @@ package io.sentry; import io.sentry.logger.ILoggerApi; +import io.sentry.metrics.IMetricsApi; import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; @@ -374,6 +375,11 @@ public void reportFullyDisplayed() { return scopes.logger(); } + @Override + public @NotNull IMetricsApi metrics() { + return scopes.metrics(); + } + @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/IScopes.java b/sentry/src/main/java/io/sentry/IScopes.java index bf78d28ecd..0a7c86fa8e 100644 --- a/sentry/src/main/java/io/sentry/IScopes.java +++ b/sentry/src/main/java/io/sentry/IScopes.java @@ -1,6 +1,7 @@ package io.sentry; import io.sentry.logger.ILoggerApi; +import io.sentry.metrics.IMetricsApi; import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; @@ -744,5 +745,8 @@ default boolean isNoOp() { @NotNull ILoggerApi logger(); + @NotNull + IMetricsApi metrics(); + 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 811e1d297a..2885d8017d 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -2,6 +2,8 @@ import io.sentry.logger.ILoggerApi; import io.sentry.logger.NoOpLoggerApi; +import io.sentry.metrics.IMetricsApi; +import io.sentry.metrics.NoOpMetricsApi; import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; @@ -331,6 +333,11 @@ public boolean isNoOp() { return NoOpLoggerApi.getInstance(); } + @Override + public @NotNull IMetricsApi metrics() { + return NoOpMetricsApi.getInstance(); + } + @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 40777da892..5abb20226a 100644 --- a/sentry/src/main/java/io/sentry/NoOpScopes.java +++ b/sentry/src/main/java/io/sentry/NoOpScopes.java @@ -2,6 +2,8 @@ import io.sentry.logger.ILoggerApi; import io.sentry.logger.NoOpLoggerApi; +import io.sentry.metrics.IMetricsApi; +import io.sentry.metrics.NoOpMetricsApi; import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; @@ -329,6 +331,11 @@ public boolean isNoOp() { return NoOpLoggerApi.getInstance(); } + @Override + public @NotNull IMetricsApi metrics() { + return NoOpMetricsApi.getInstance(); + } + @Override public void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result) {} } diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index c8afde59cc..374ecbfdb5 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -5,6 +5,8 @@ import io.sentry.hints.SessionStartHint; import io.sentry.logger.ILoggerApi; import io.sentry.logger.LoggerApi; +import io.sentry.metrics.IMetricsApi; +import io.sentry.metrics.MetricsApi; import io.sentry.protocol.*; import io.sentry.transport.RateLimiter; import io.sentry.util.HintUtils; @@ -31,6 +33,7 @@ public final class Scopes implements IScopes { private final @NotNull CombinedScopeView combinedScope; private final @NotNull ILoggerApi logger; + private final @NotNull IMetricsApi metrics; public Scopes( final @NotNull IScope scope, @@ -57,6 +60,7 @@ private Scopes( validateOptions(options); this.compositePerformanceCollector = options.getCompositePerformanceCollector(); this.logger = new LoggerApi(this); + this.metrics = new MetricsApi(this); } public @NotNull String getCreator() { @@ -1220,6 +1224,11 @@ public void reportFullyDisplayed() { return logger; } + @Override + public @NotNull IMetricsApi metrics() { + return metrics; + } + @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 99e70694ee..ba7e74d23b 100644 --- a/sentry/src/main/java/io/sentry/ScopesAdapter.java +++ b/sentry/src/main/java/io/sentry/ScopesAdapter.java @@ -1,6 +1,7 @@ package io.sentry; import io.sentry.logger.ILoggerApi; +import io.sentry.metrics.IMetricsApi; import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; @@ -386,6 +387,11 @@ public void reportFullyDisplayed() { return Sentry.getCurrentScopes().logger(); } + @Override + public @NotNull IMetricsApi metrics() { + return Sentry.getCurrentScopes().metrics(); + } + @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 f726c1a602..178f97a3b3 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -14,6 +14,7 @@ import io.sentry.internal.modules.NoOpModulesLoader; import io.sentry.internal.modules.ResourcesModulesLoader; import io.sentry.logger.ILoggerApi; +import io.sentry.metrics.IMetricsApi; import io.sentry.opentelemetry.OpenTelemetryUtil; import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; @@ -1344,6 +1345,11 @@ public static IDistributionApi distribution() { return getCurrentScopes().getScope().getOptions().getDistributionController(); } + @NotNull + public static IMetricsApi metrics() { + return getCurrentScopes().metrics(); + } + public static void showUserFeedbackDialog() { showUserFeedbackDialog(null); } diff --git a/sentry/src/main/java/io/sentry/metrics/IMetricsApi.java b/sentry/src/main/java/io/sentry/metrics/IMetricsApi.java new file mode 100644 index 0000000000..ddd6bb9d41 --- /dev/null +++ b/sentry/src/main/java/io/sentry/metrics/IMetricsApi.java @@ -0,0 +1,11 @@ +package io.sentry.metrics; + +import org.jetbrains.annotations.NotNull; + +public interface IMetricsApi { + + void count(@NotNull final String name); + // distribution + // gauge + // +} diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java new file mode 100644 index 0000000000..7b878de3d2 --- /dev/null +++ b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java @@ -0,0 +1,18 @@ +package io.sentry.metrics; + +import io.sentry.Scopes; +import org.jetbrains.annotations.NotNull; + +public final class MetricsApi implements IMetricsApi { + + private final @NotNull Scopes scopes; + + public MetricsApi(final @NotNull Scopes scopes) { + this.scopes = scopes; + } + + @Override + public void count(@NotNull String name) { + scopes.getOptions(); + } +} diff --git a/sentry/src/main/java/io/sentry/metrics/NoOpMetricsApi.java b/sentry/src/main/java/io/sentry/metrics/NoOpMetricsApi.java new file mode 100644 index 0000000000..f6977b6953 --- /dev/null +++ b/sentry/src/main/java/io/sentry/metrics/NoOpMetricsApi.java @@ -0,0 +1,16 @@ +package io.sentry.metrics; + +import org.jetbrains.annotations.NotNull; + +public final class NoOpMetricsApi implements IMetricsApi { + private static final NoOpMetricsApi instance = new NoOpMetricsApi(); + + private NoOpMetricsApi() {} + + public static NoOpMetricsApi getInstance() { + return instance; + } + + @Override + public void count(@NotNull String name) {} +} From a4b97cc89872a09938609e0ad86a89b25c0f3c6c Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 17 Dec 2025 16:11:13 +0100 Subject: [PATCH 03/40] Add transport types for metrics --- .../java/io/sentry/SentryEnvelopeItem.java | 30 ++ .../java/io/sentry/SentryMetricsEvent.java | 293 ++++++++++++++++++ .../java/io/sentry/SentryMetricsEvents.java | 100 ++++++ .../main/java/io/sentry/SentryOptions.java | 3 +- 4 files changed, 424 insertions(+), 2 deletions(-) create mode 100644 sentry/src/main/java/io/sentry/SentryMetricsEvent.java create mode 100644 sentry/src/main/java/io/sentry/SentryMetricsEvents.java diff --git a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java index 04bf74fcfe..155c40ae7c 100644 --- a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java +++ b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java @@ -546,6 +546,36 @@ public static SentryEnvelopeItem fromLogs( return new SentryEnvelopeItem(itemHeader, () -> cachedItem.getBytes()); } + public static SentryEnvelopeItem fromMetrics( + final @NotNull ISerializer serializer, final @NotNull SentryMetricsEvents metricsEvents) { + Objects.requireNonNull(serializer, "ISerializer is required."); + Objects.requireNonNull(metricsEvents, "SentryMetricsEvents is required."); + + final CachedItem cachedItem = + new CachedItem( + () -> { + try (final ByteArrayOutputStream stream = new ByteArrayOutputStream(); + final Writer writer = new BufferedWriter(new OutputStreamWriter(stream, UTF_8))) { + serializer.serialize(metricsEvents, writer); + return stream.toByteArray(); + } + }); + + SentryEnvelopeItemHeader itemHeader = + new SentryEnvelopeItemHeader( + SentryItemType.TraceMetric, + () -> cachedItem.getBytes().length, + "application/vnd.sentry.items.trace-metric+json", + null, + null, + null, + metricsEvents.getItems().size()); + + // avoid method refs on Android due to some issues with older AGP setups + // noinspection Convert2MethodRef + return new SentryEnvelopeItem(itemHeader, () -> cachedItem.getBytes()); + } + private static class CachedItem { private @Nullable byte[] bytes; private final @Nullable Callable dataFactory; diff --git a/sentry/src/main/java/io/sentry/SentryMetricsEvent.java b/sentry/src/main/java/io/sentry/SentryMetricsEvent.java new file mode 100644 index 0000000000..21050f9bcb --- /dev/null +++ b/sentry/src/main/java/io/sentry/SentryMetricsEvent.java @@ -0,0 +1,293 @@ +package io.sentry; + +import static io.sentry.DateUtils.doubleToBigDecimal; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import io.sentry.protocol.SentryId; +import io.sentry.vendor.gson.stream.JsonToken; + +public final class SentryMetricsEvent implements JsonUnknown, JsonSerializable { + + private @NotNull SentryId traceId; + private @Nullable SpanId spanId; + /** + * Timestamp in seconds (epoch time) indicating when the metric was recorded. + */ + private @NotNull Double timestamp; + /** + * The name of the metric. + * This should follow a hierarchical naming convention using dots as separators + * (e.g., api.response_time, db.query.duration). + */ + private @NotNull String name; + /** + * The unit of measurement for the metric value. + */ + private @Nullable String unit; + /** + * The type of metric. One of: + * - counter: A metric that increments counts + * - gauge: A metric that tracks a value that can go up or down + * - distribution: A metric that tracks the statistical distribution of values + */ + private @NotNull String type; + /** + * The numeric value of the metric. The interpretation depends on the metric type: + * - For counter metrics: the count to increment by (should default to 1) + * - For gauge metrics: the current value + * - For distribution metrics: a single measured value + */ + private @NotNull Double value; + + private @Nullable Map attributes; + private @Nullable Map unknown; + + public SentryMetricsEvent( + final @NotNull SentryId traceId, + final @NotNull SentryDate timestamp, + final @NotNull String name, + final @NotNull String type, + final @NotNull Double value) { + this(traceId, DateUtils.nanosToSeconds(timestamp.nanoTimestamp()), name, type, value); + } + + public SentryMetricsEvent( + final @NotNull SentryId traceId, + final @NotNull Double timestamp, + final @NotNull String name, + final @NotNull String type, + final @NotNull Double value) { + this.traceId = traceId; + this.timestamp = timestamp; + this.name = name; + this.type = type; + this.value = value; + } + + @NotNull + public Double getTimestamp() { + return timestamp; + } + + public void setTimestamp(final @NotNull Double timestamp) { + this.timestamp = timestamp; + } + + public @NotNull String getName() { + return name; + } + + public void setName(@NotNull String name) { + this.name = name; + } + + public @NotNull String getType() { + return type; + } + + public void setType(@NotNull String type) { + this.type = type; + } + + public @Nullable String getUnit() { + return unit; + } + + public void setUnit(@Nullable String unit) { + this.unit = unit; + } + + public @Nullable SpanId getSpanId() { + return spanId; + } + + public void setSpanId(@Nullable SpanId spanId) { + this.spanId = spanId; + } + + public @NotNull Double getValue() { + return value; + } + + public void setValue(@NotNull Double value) { + this.value = value; + } + + public @Nullable Map getAttributes() { + return attributes; + } + + public void setAttributes(final @Nullable Map attributes) { + this.attributes = attributes; + } + + public void setAttribute( + final @Nullable String key, final @Nullable SentryLogEventAttributeValue value) { + if (key == null) { + return; + } + if (this.attributes == null) { + this.attributes = new HashMap<>(); + } + this.attributes.put(key, value); + } + + // region json + public static final class JsonKeys { + public static final String TIMESTAMP = "timestamp"; + public static final String TRACE_ID = "trace_id"; + public static final String SPAN_ID = "span_id"; + public static final String NAME = "name"; + public static final String TYPE = "type"; + public static final String UNIT = "unit"; + public static final String VALUE = "value"; + public static final String ATTRIBUTES = "attributes"; + } + + @Override + @SuppressWarnings("JdkObsolete") + public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger) + throws IOException { + writer.beginObject(); + writer.name(JsonKeys.TIMESTAMP).value(logger, doubleToBigDecimal(timestamp)); + writer.name(JsonKeys.TYPE).value(type); + writer.name(JsonKeys.NAME).value(name); + writer.name(JsonKeys.VALUE).value(value); + writer.name(JsonKeys.TRACE_ID).value(logger, traceId); + if (spanId != null) { + writer.name(JsonKeys.SPAN_ID).value(logger, spanId); + } + if (unit != null) { + writer.name(JsonKeys.UNIT).value(logger, unit); + } + if (attributes != null) { + writer.name(JsonKeys.ATTRIBUTES).value(logger, attributes); + } + + if (unknown != null) { + for (String key : unknown.keySet()) { + Object value = unknown.get(key); + writer.name(key).value(logger, value); + } + } + writer.endObject(); + } + + @Override + public @Nullable Map getUnknown() { + return unknown; + } + + @Override + public void setUnknown(final @Nullable Map unknown) { + this.unknown = unknown; + } + + public static final class Deserializer implements JsonDeserializer { + + @SuppressWarnings("unchecked") + @Override + public @NotNull SentryMetricsEvent deserialize( + final @NotNull ObjectReader reader, final @NotNull ILogger logger) throws Exception { + @Nullable Map unknown = null; + @Nullable SentryId traceId = null; + @Nullable SpanId spanId = null; + @Nullable Double timestamp = null; + @Nullable String type = null; + @Nullable String name = null; + @Nullable String unit = null; + @Nullable Double value = null; + @Nullable Map attributes = null; + + reader.beginObject(); + while (reader.peek() == JsonToken.NAME) { + final String nextName = reader.nextName(); + switch (nextName) { + case JsonKeys.TRACE_ID: + traceId = reader.nextOrNull(logger, new SentryId.Deserializer()); + break; + case JsonKeys.SPAN_ID: + spanId = reader.nextOrNull(logger, new SpanId.Deserializer()); + break; + case JsonKeys.TIMESTAMP: + timestamp = reader.nextDoubleOrNull(); + break; + case JsonKeys.TYPE: + type = reader.nextStringOrNull(); + break; + case JsonKeys.NAME: + name = reader.nextStringOrNull(); + break; + case JsonKeys.UNIT: + unit = reader.nextStringOrNull(); + break; + case JsonKeys.VALUE: + value = reader.nextDoubleOrNull(); + break; + case JsonKeys.ATTRIBUTES: + attributes = + reader.nextMapOrNull(logger, new SentryLogEventAttributeValue.Deserializer()); + break; + default: + if (unknown == null) { + unknown = new HashMap<>(); + } + reader.nextUnknown(logger, unknown, nextName); + break; + } + } + reader.endObject(); + + if (traceId == null) { + String message = "Missing required field \"" + JsonKeys.TRACE_ID + "\""; + Exception exception = new IllegalStateException(message); + logger.log(SentryLevel.ERROR, message, exception); + throw exception; + } + + if (timestamp == null) { + String message = "Missing required field \"" + JsonKeys.TIMESTAMP + "\""; + Exception exception = new IllegalStateException(message); + logger.log(SentryLevel.ERROR, message, exception); + throw exception; + } + + if (type == null) { + String message = "Missing required field \"" + JsonKeys.TYPE + "\""; + Exception exception = new IllegalStateException(message); + logger.log(SentryLevel.ERROR, message, exception); + throw exception; + } + + if (name == null) { + String message = "Missing required field \"" + JsonKeys.NAME + "\""; + Exception exception = new IllegalStateException(message); + logger.log(SentryLevel.ERROR, message, exception); + throw exception; + } + + if (value == null) { + String message = "Missing required field \"" + JsonKeys.VALUE + "\""; + Exception exception = new IllegalStateException(message); + logger.log(SentryLevel.ERROR, message, exception); + throw exception; + } + + final SentryMetricsEvent logEvent = new SentryMetricsEvent(traceId, timestamp, name, type, value); + + logEvent.setAttributes(attributes); + logEvent.setSpanId(spanId); + logEvent.setUnit(unit); + logEvent.setUnknown(unknown); + + return logEvent; + } + } + // endregion json +} diff --git a/sentry/src/main/java/io/sentry/SentryMetricsEvents.java b/sentry/src/main/java/io/sentry/SentryMetricsEvents.java new file mode 100644 index 0000000000..883f8e12d7 --- /dev/null +++ b/sentry/src/main/java/io/sentry/SentryMetricsEvents.java @@ -0,0 +1,100 @@ +package io.sentry; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.sentry.vendor.gson.stream.JsonToken; + +public final class SentryMetricsEvents implements JsonUnknown, JsonSerializable { + + private @NotNull List items; + private @Nullable Map unknown; + + public SentryMetricsEvents(final @NotNull List items) { + this.items = items; + } + + public @NotNull List getItems() { + return items; + } + + // region json + public static final class JsonKeys { + public static final String ITEMS = "items"; + } + + @Override + @SuppressWarnings("JdkObsolete") + public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger) + throws IOException { + writer.beginObject(); + + writer.name(JsonKeys.ITEMS).value(logger, items); + + if (unknown != null) { + for (String key : unknown.keySet()) { + Object value = unknown.get(key); + writer.name(key).value(logger, value); + } + } + + writer.endObject(); + } + + @Override + public @Nullable Map getUnknown() { + return unknown; + } + + @Override + public void setUnknown(final @Nullable Map unknown) { + this.unknown = unknown; + } + + public static final class Deserializer implements JsonDeserializer { + + @SuppressWarnings("unchecked") + @Override + public @NotNull SentryMetricsEvents deserialize( + final @NotNull ObjectReader reader, final @NotNull ILogger logger) throws Exception { + @Nullable Map unknown = null; + @Nullable List items = null; + + reader.beginObject(); + while (reader.peek() == JsonToken.NAME) { + final String nextName = reader.nextName(); + switch (nextName) { + case JsonKeys.ITEMS: + items = reader.nextListOrNull(logger, new SentryMetricsEvent.Deserializer()); + break; + default: + if (unknown == null) { + unknown = new HashMap<>(); + } + reader.nextUnknown(logger, unknown, nextName); + break; + } + } + reader.endObject(); + + if (items == null) { + String message = "Missing required field \"" + JsonKeys.ITEMS + "\""; + Exception exception = new IllegalStateException(message); + logger.log(SentryLevel.ERROR, message, exception); + throw exception; + } + + final SentryMetricsEvents metricsEvent = new SentryMetricsEvents(items); + + metricsEvent.setUnknown(unknown); + + return metricsEvent; + } + } + // endregion json +} diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 6fa246c15c..e832a30726 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -3794,9 +3794,8 @@ public interface BeforeSendMetricCallback { * @param metric the metric * @return the original metric, mutated metric or null if metric was dropped */ - // TODO replace with SentryMetric @Nullable - Object execute(@NotNull Object metric); + SentryMetricsEvents execute(@NotNull SentryMetricsEvents metric); } } From d3c74520c4d0be20bd07b6b256c142495afc911a Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 17 Dec 2025 16:10:40 +0100 Subject: [PATCH 04/40] Enable metrics by default --- sentry/src/main/java/io/sentry/SentryOptions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 2575328c70..6fa246c15c 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -3742,7 +3742,7 @@ public interface BeforeSendLogCallback { public static final class Metrics { /** Whether Sentry Metrics feature is enabled and metrics are sent to Sentry. */ - private boolean enable = false; + private boolean enable = true; /** * This function is called with a metric key and tags and can return false to skip sending the From 14ea96a6bacc7052e02f9884d7d5caad793a1f85 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Mon, 22 Dec 2025 12:52:15 +0000 Subject: [PATCH 05/40] Format code --- .../java/io/sentry/SentryMetricsEvent.java | 43 +++++++++---------- .../java/io/sentry/SentryMetricsEvents.java | 8 ++-- 2 files changed, 23 insertions(+), 28 deletions(-) diff --git a/sentry/src/main/java/io/sentry/SentryMetricsEvent.java b/sentry/src/main/java/io/sentry/SentryMetricsEvent.java index 21050f9bcb..1000d58653 100644 --- a/sentry/src/main/java/io/sentry/SentryMetricsEvent.java +++ b/sentry/src/main/java/io/sentry/SentryMetricsEvent.java @@ -2,45 +2,41 @@ import static io.sentry.DateUtils.doubleToBigDecimal; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - +import io.sentry.protocol.SentryId; +import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; import java.util.HashMap; import java.util.Map; - -import io.sentry.protocol.SentryId; -import io.sentry.vendor.gson.stream.JsonToken; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public final class SentryMetricsEvent implements JsonUnknown, JsonSerializable { private @NotNull SentryId traceId; private @Nullable SpanId spanId; - /** - * Timestamp in seconds (epoch time) indicating when the metric was recorded. - */ + + /** Timestamp in seconds (epoch time) indicating when the metric was recorded. */ private @NotNull Double timestamp; + /** - * The name of the metric. - * This should follow a hierarchical naming convention using dots as separators - * (e.g., api.response_time, db.query.duration). + * The name of the metric. This should follow a hierarchical naming convention using dots as + * separators (e.g., api.response_time, db.query.duration). */ private @NotNull String name; - /** - * The unit of measurement for the metric value. - */ + + /** The unit of measurement for the metric value. */ private @Nullable String unit; + /** - * The type of metric. One of: - * - counter: A metric that increments counts - * - gauge: A metric that tracks a value that can go up or down - * - distribution: A metric that tracks the statistical distribution of values + * The type of metric. One of: - counter: A metric that increments counts - gauge: A metric that + * tracks a value that can go up or down - distribution: A metric that tracks the statistical + * distribution of values */ private @NotNull String type; + /** - * The numeric value of the metric. The interpretation depends on the metric type: - * - For counter metrics: the count to increment by (should default to 1) - * - For gauge metrics: the current value + * The numeric value of the metric. The interpretation depends on the metric type: - For counter + * metrics: the count to increment by (should default to 1) - For gauge metrics: the current value * - For distribution metrics: a single measured value */ private @NotNull Double value; @@ -279,7 +275,8 @@ public static final class Deserializer implements JsonDeserializer Date: Thu, 18 Dec 2025 10:25:24 +0100 Subject: [PATCH 06/40] Add batch processor for Metrics --- .../core/SessionTrackingIntegrationTest.kt | 5 + sentry/api/sentry.api | 98 ++++++++++- .../main/java/io/sentry/ISentryClient.java | 3 + .../main/java/io/sentry/NoOpSentryClient.java | 6 + .../src/main/java/io/sentry/SentryClient.java | 26 ++- .../DefaultMetricsBatchProcessorFactory.java | 13 ++ .../metrics/IMetricsBatchProcessor.java | 17 ++ .../IMetricsBatchProcessorFactory.java | 12 ++ .../sentry/metrics/MetricsBatchProcessor.java | 155 ++++++++++++++++++ .../metrics/NoOpMetricsBatchProcessor.java | 30 ++++ 10 files changed, 363 insertions(+), 2 deletions(-) create mode 100644 sentry/src/main/java/io/sentry/metrics/DefaultMetricsBatchProcessorFactory.java create mode 100644 sentry/src/main/java/io/sentry/metrics/IMetricsBatchProcessor.java create mode 100644 sentry/src/main/java/io/sentry/metrics/IMetricsBatchProcessorFactory.java create mode 100644 sentry/src/main/java/io/sentry/metrics/MetricsBatchProcessor.java create mode 100644 sentry/src/main/java/io/sentry/metrics/NoOpMetricsBatchProcessor.java diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt index bdb328e242..b4235aa6b9 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt @@ -18,6 +18,7 @@ import io.sentry.SentryEnvelope import io.sentry.SentryEvent import io.sentry.SentryLogEvent import io.sentry.SentryLogEvents +import io.sentry.SentryMetricsEvents import io.sentry.SentryReplayEvent import io.sentry.Session import io.sentry.TraceContext @@ -192,6 +193,10 @@ class SessionTrackingIntegrationTest { TODO("Not yet implemented") } + override fun captureBatchedMetricsEvents(metricsEvents: SentryMetricsEvents) { + TODO("Not yet implemented") + } + override fun getRateLimiter(): RateLimiter? { TODO("Not yet implemented") } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index f947368b75..3f5a20a763 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -1029,6 +1029,7 @@ public abstract interface class io/sentry/IScopesStorage { public abstract interface class io/sentry/ISentryClient { public abstract fun captureBatchedLogEvents (Lio/sentry/SentryLogEvents;)V + public abstract fun captureBatchedMetricsEvents (Lio/sentry/SentryMetricsEvents;)V public abstract fun captureCheckIn (Lio/sentry/CheckIn;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureEnvelope (Lio/sentry/SentryEnvelope;)Lio/sentry/protocol/SentryId; public abstract fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; @@ -2852,6 +2853,7 @@ public final class io/sentry/SentryBaseEvent$Serializer { public final class io/sentry/SentryClient : io/sentry/ISentryClient { public fun (Lio/sentry/SentryOptions;)V public fun captureBatchedLogEvents (Lio/sentry/SentryLogEvents;)V + public fun captureBatchedMetricsEvents (Lio/sentry/SentryMetricsEvents;)V public fun captureCheckIn (Lio/sentry/CheckIn;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; @@ -2938,6 +2940,7 @@ public final class io/sentry/SentryEnvelopeItem { public static fun fromClientReport (Lio/sentry/ISerializer;Lio/sentry/clientreport/ClientReport;)Lio/sentry/SentryEnvelopeItem; public static fun fromEvent (Lio/sentry/ISerializer;Lio/sentry/SentryBaseEvent;)Lio/sentry/SentryEnvelopeItem; public static fun fromLogs (Lio/sentry/ISerializer;Lio/sentry/SentryLogEvents;)Lio/sentry/SentryEnvelopeItem; + public static fun fromMetrics (Lio/sentry/ISerializer;Lio/sentry/SentryMetricsEvents;)Lio/sentry/SentryEnvelopeItem; public static fun fromProfileChunk (Lio/sentry/ProfileChunk;Lio/sentry/ISerializer;)Lio/sentry/SentryEnvelopeItem; public static fun fromProfileChunk (Lio/sentry/ProfileChunk;Lio/sentry/ISerializer;Lio/sentry/IProfileConverter;)Lio/sentry/SentryEnvelopeItem; public static fun fromProfilingTrace (Lio/sentry/ProfilingTraceData;JLio/sentry/ISerializer;)Lio/sentry/SentryEnvelopeItem; @@ -3322,6 +3325,66 @@ public final class io/sentry/SentryLongDate : io/sentry/SentryDate { public fun nanoTimestamp ()J } +public final class io/sentry/SentryMetricsEvent : io/sentry/JsonSerializable, io/sentry/JsonUnknown { + public fun (Lio/sentry/protocol/SentryId;Lio/sentry/SentryDate;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;)V + public fun (Lio/sentry/protocol/SentryId;Ljava/lang/Double;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Double;)V + public fun getAttributes ()Ljava/util/Map; + public fun getName ()Ljava/lang/String; + public fun getSpanId ()Lio/sentry/SpanId; + public fun getTimestamp ()Ljava/lang/Double; + public fun getType ()Ljava/lang/String; + public fun getUnit ()Ljava/lang/String; + public fun getUnknown ()Ljava/util/Map; + public fun getValue ()Ljava/lang/Double; + public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public fun setAttribute (Ljava/lang/String;Lio/sentry/SentryLogEventAttributeValue;)V + public fun setAttributes (Ljava/util/Map;)V + public fun setName (Ljava/lang/String;)V + public fun setSpanId (Lio/sentry/SpanId;)V + public fun setTimestamp (Ljava/lang/Double;)V + public fun setType (Ljava/lang/String;)V + public fun setUnit (Ljava/lang/String;)V + public fun setUnknown (Ljava/util/Map;)V + public fun setValue (Ljava/lang/Double;)V +} + +public final class io/sentry/SentryMetricsEvent$Deserializer : io/sentry/JsonDeserializer { + public fun ()V + public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/SentryMetricsEvent; + public synthetic fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; +} + +public final class io/sentry/SentryMetricsEvent$JsonKeys { + public static final field ATTRIBUTES Ljava/lang/String; + public static final field NAME Ljava/lang/String; + public static final field SPAN_ID Ljava/lang/String; + public static final field TIMESTAMP Ljava/lang/String; + public static final field TRACE_ID Ljava/lang/String; + public static final field TYPE Ljava/lang/String; + public static final field UNIT Ljava/lang/String; + public static final field VALUE Ljava/lang/String; + public fun ()V +} + +public final class io/sentry/SentryMetricsEvents : io/sentry/JsonSerializable, io/sentry/JsonUnknown { + public fun (Ljava/util/List;)V + public fun getItems ()Ljava/util/List; + public fun getUnknown ()Ljava/util/Map; + public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public fun setUnknown (Ljava/util/Map;)V +} + +public final class io/sentry/SentryMetricsEvents$Deserializer : io/sentry/JsonDeserializer { + public fun ()V + public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/SentryMetricsEvents; + public synthetic fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; +} + +public final class io/sentry/SentryMetricsEvents$JsonKeys { + public static final field ITEMS Ljava/lang/String; + public fun ()V +} + public final class io/sentry/SentryNanotimeDate : io/sentry/SentryDate { public fun ()V public fun (Ljava/util/Date;J)V @@ -3698,7 +3761,7 @@ public final class io/sentry/SentryOptions$Metrics { } public abstract interface class io/sentry/SentryOptions$Metrics$BeforeSendMetricCallback { - public abstract fun execute (Ljava/lang/Object;)Ljava/lang/Object; + public abstract fun execute (Lio/sentry/SentryMetricsEvents;)Lio/sentry/SentryMetricsEvents; } public abstract interface class io/sentry/SentryOptions$OnDiscardCallback { @@ -5131,20 +5194,53 @@ public final class io/sentry/logger/SentryLogParameters { public fun setTimestamp (Lio/sentry/SentryDate;)V } +public final class io/sentry/metrics/DefaultMetricsBatchProcessorFactory : io/sentry/metrics/IMetricsBatchProcessorFactory { + public fun ()V + public fun create (Lio/sentry/SentryOptions;Lio/sentry/SentryClient;)Lio/sentry/metrics/IMetricsBatchProcessor; +} + public abstract interface class io/sentry/metrics/IMetricsApi { public abstract fun count (Ljava/lang/String;)V } +public abstract interface class io/sentry/metrics/IMetricsBatchProcessor { + public abstract fun add (Lio/sentry/SentryMetricsEvent;)V + public abstract fun close (Z)V + public abstract fun flush (J)V +} + +public abstract interface class io/sentry/metrics/IMetricsBatchProcessorFactory { + public abstract fun create (Lio/sentry/SentryOptions;Lio/sentry/SentryClient;)Lio/sentry/metrics/IMetricsBatchProcessor; +} + public final class io/sentry/metrics/MetricsApi : io/sentry/metrics/IMetricsApi { public fun (Lio/sentry/Scopes;)V public fun count (Ljava/lang/String;)V } +public class io/sentry/metrics/MetricsBatchProcessor : io/sentry/metrics/IMetricsBatchProcessor { + public static final field FLUSH_AFTER_MS I + public static final field MAX_BATCH_SIZE I + public static final field MAX_QUEUE_SIZE I + protected final field options Lio/sentry/SentryOptions; + public fun (Lio/sentry/SentryOptions;Lio/sentry/ISentryClient;)V + public fun add (Lio/sentry/SentryMetricsEvent;)V + public fun close (Z)V + public fun flush (J)V +} + public final class io/sentry/metrics/NoOpMetricsApi : io/sentry/metrics/IMetricsApi { public fun count (Ljava/lang/String;)V public static fun getInstance ()Lio/sentry/metrics/NoOpMetricsApi; } +public final class io/sentry/metrics/NoOpMetricsBatchProcessor : io/sentry/metrics/IMetricsBatchProcessor { + public fun add (Lio/sentry/SentryMetricsEvent;)V + public fun close (Z)V + public fun flush (J)V + public static fun getInstance ()Lio/sentry/metrics/NoOpMetricsBatchProcessor; +} + public final class io/sentry/opentelemetry/OpenTelemetryUtil { public fun ()V public static fun applyIgnoredSpanOrigins (Lio/sentry/SentryOptions;)V diff --git a/sentry/src/main/java/io/sentry/ISentryClient.java b/sentry/src/main/java/io/sentry/ISentryClient.java index c2bc05516f..cd9ce85e9a 100644 --- a/sentry/src/main/java/io/sentry/ISentryClient.java +++ b/sentry/src/main/java/io/sentry/ISentryClient.java @@ -308,6 +308,9 @@ SentryId captureProfileChunk( @ApiStatus.Internal void captureBatchedLogEvents(@NotNull SentryLogEvents logEvents); + @ApiStatus.Internal + void captureBatchedMetricsEvents(@NotNull SentryMetricsEvents metricsEvents); + @ApiStatus.Internal @Nullable RateLimiter getRateLimiter(); diff --git a/sentry/src/main/java/io/sentry/NoOpSentryClient.java b/sentry/src/main/java/io/sentry/NoOpSentryClient.java index 17e4becbc7..cd2ed885ca 100644 --- a/sentry/src/main/java/io/sentry/NoOpSentryClient.java +++ b/sentry/src/main/java/io/sentry/NoOpSentryClient.java @@ -94,6 +94,12 @@ public void captureBatchedLogEvents(@NotNull SentryLogEvents logEvents) { // do nothing } + @ApiStatus.Internal + @Override + public void captureBatchedMetricsEvents(@NotNull SentryMetricsEvents metricsEvents) { + // do nothing + } + @Override public @Nullable RateLimiter getRateLimiter() { return null; diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 0a767ed1b8..ed6c5cbe03 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -693,6 +693,19 @@ public void captureUserFeedback(final @NotNull UserFeedback userFeedback) { return new SentryEnvelope(envelopeHeader, envelopeItems); } + private @NotNull SentryEnvelope buildEnvelope(final @NotNull SentryMetricsEvents metricsEvents) { + final List envelopeItems = new ArrayList<>(); + + final SentryEnvelopeItem metricsItem = + SentryEnvelopeItem.fromMetrics(options.getSerializer(), metricsEvents); + envelopeItems.add(metricsItem); + + final SentryEnvelopeHeader envelopeHeader = + new SentryEnvelopeHeader(null, options.getSdkVersion(), null); + + return new SentryEnvelope(envelopeHeader, envelopeItems); + } + private @NotNull SentryEnvelope buildEnvelope( final @NotNull SentryReplayEvent event, final @Nullable ReplayRecording replayRecording, @@ -1218,7 +1231,18 @@ public void captureBatchedLogEvents(final @NotNull SentryLogEvents logEvents) { final @NotNull SentryEnvelope envelope = buildEnvelope(logEvents); sendEnvelope(envelope, null); } catch (IOException e) { - options.getLogger().log(SentryLevel.WARNING, e, "Capturing log failed."); + options.getLogger().log(SentryLevel.WARNING, e, "Capturing logs failed."); + } + } + + @ApiStatus.Internal + @Override + public void captureBatchedMetricsEvents(final @NotNull SentryMetricsEvents metricsEvents) { + try { + final @NotNull SentryEnvelope envelope = buildEnvelope(metricsEvents); + sendEnvelope(envelope, null); + } catch (IOException e) { + options.getLogger().log(SentryLevel.WARNING, e, "Capturing metrics failed."); } } diff --git a/sentry/src/main/java/io/sentry/metrics/DefaultMetricsBatchProcessorFactory.java b/sentry/src/main/java/io/sentry/metrics/DefaultMetricsBatchProcessorFactory.java new file mode 100644 index 0000000000..dee7e5b511 --- /dev/null +++ b/sentry/src/main/java/io/sentry/metrics/DefaultMetricsBatchProcessorFactory.java @@ -0,0 +1,13 @@ +package io.sentry.metrics; + +import io.sentry.SentryClient; +import io.sentry.SentryOptions; +import org.jetbrains.annotations.NotNull; + +public final class DefaultMetricsBatchProcessorFactory implements IMetricsBatchProcessorFactory { + @Override + public @NotNull IMetricsBatchProcessor create( + @NotNull SentryOptions options, @NotNull SentryClient client) { + return new MetricsBatchProcessor(options, client); + } +} diff --git a/sentry/src/main/java/io/sentry/metrics/IMetricsBatchProcessor.java b/sentry/src/main/java/io/sentry/metrics/IMetricsBatchProcessor.java new file mode 100644 index 0000000000..f019c2a89b --- /dev/null +++ b/sentry/src/main/java/io/sentry/metrics/IMetricsBatchProcessor.java @@ -0,0 +1,17 @@ +package io.sentry.metrics; + +import io.sentry.SentryMetricsEvent; +import org.jetbrains.annotations.NotNull; + +public interface IMetricsBatchProcessor { + void add(@NotNull SentryMetricsEvent event); + + void close(boolean isRestarting); + + /** + * Flushes log events. + * + * @param timeoutMillis time in milliseconds + */ + void flush(long timeoutMillis); +} diff --git a/sentry/src/main/java/io/sentry/metrics/IMetricsBatchProcessorFactory.java b/sentry/src/main/java/io/sentry/metrics/IMetricsBatchProcessorFactory.java new file mode 100644 index 0000000000..a909512864 --- /dev/null +++ b/sentry/src/main/java/io/sentry/metrics/IMetricsBatchProcessorFactory.java @@ -0,0 +1,12 @@ +package io.sentry.metrics; + +import io.sentry.SentryClient; +import io.sentry.SentryOptions; +import org.jetbrains.annotations.NotNull; + +public interface IMetricsBatchProcessorFactory { + + @NotNull + IMetricsBatchProcessor create( + final @NotNull SentryOptions options, final @NotNull SentryClient client); +} diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsBatchProcessor.java b/sentry/src/main/java/io/sentry/metrics/MetricsBatchProcessor.java new file mode 100644 index 0000000000..10e58000e7 --- /dev/null +++ b/sentry/src/main/java/io/sentry/metrics/MetricsBatchProcessor.java @@ -0,0 +1,155 @@ +package io.sentry.metrics; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.DataCategory; +import io.sentry.ISentryClient; +import io.sentry.ISentryExecutorService; +import io.sentry.ISentryLifecycleToken; +import io.sentry.SentryExecutorService; +import io.sentry.SentryLevel; +import io.sentry.SentryMetricsEvent; +import io.sentry.SentryMetricsEvents; +import io.sentry.SentryOptions; +import io.sentry.clientreport.DiscardReason; +import io.sentry.transport.ReusableCountLatch; +import io.sentry.util.AutoClosableReentrantLock; +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@Open +public class MetricsBatchProcessor implements IMetricsBatchProcessor { + + public static final int FLUSH_AFTER_MS = 5000; + public static final int MAX_BATCH_SIZE = 1000; + public static final int MAX_QUEUE_SIZE = 10000; + + protected final @NotNull SentryOptions options; + private final @NotNull ISentryClient client; + private final @NotNull Queue queue; + private final @NotNull ISentryExecutorService executorService; + private volatile @Nullable Future scheduledFlush; + private static final @NotNull AutoClosableReentrantLock scheduleLock = + new AutoClosableReentrantLock(); + private volatile boolean hasScheduled = false; + + private final @NotNull ReusableCountLatch pendingCount = new ReusableCountLatch(); + + public MetricsBatchProcessor( + final @NotNull SentryOptions options, final @NotNull ISentryClient client) { + this.options = options; + this.client = client; + this.queue = new ConcurrentLinkedQueue<>(); + this.executorService = new SentryExecutorService(options); + } + + @Override + public void add(final @NotNull SentryMetricsEvent metricsEvent) { + if (pendingCount.getCount() >= MAX_QUEUE_SIZE) { + options + .getClientReportRecorder() + .recordLostEvent(DiscardReason.QUEUE_OVERFLOW, DataCategory.TraceMetric); + return; + } + pendingCount.increment(); + queue.offer(metricsEvent); + maybeSchedule(false, false); + } + + @SuppressWarnings("FutureReturnValueIgnored") + @Override + public void close(final boolean isRestarting) { + if (isRestarting) { + maybeSchedule(true, true); + executorService.submit(() -> executorService.close(options.getShutdownTimeoutMillis())); + } else { + executorService.close(options.getShutdownTimeoutMillis()); + while (!queue.isEmpty()) { + flushBatch(); + } + } + } + + private void maybeSchedule(boolean forceSchedule, boolean immediately) { + if (hasScheduled && !forceSchedule) { + return; + } + try (final @NotNull ISentryLifecycleToken ignored = scheduleLock.acquire()) { + final @Nullable Future latestScheduledFlush = scheduledFlush; + if (forceSchedule + || latestScheduledFlush == null + || latestScheduledFlush.isDone() + || latestScheduledFlush.isCancelled()) { + hasScheduled = true; + final int flushAfterMs = immediately ? 0 : FLUSH_AFTER_MS; + try { + scheduledFlush = executorService.schedule(new BatchRunnable(), flushAfterMs); + } catch (RejectedExecutionException e) { + hasScheduled = false; + options + .getLogger() + .log(SentryLevel.WARNING, "Metrics batch processor flush task rejected", e); + } + } + } + } + + @Override + public void flush(long timeoutMillis) { + maybeSchedule(true, true); + try { + pendingCount.waitTillZero(timeoutMillis, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + options.getLogger().log(SentryLevel.ERROR, "Failed to flush metrics events", e); + Thread.currentThread().interrupt(); + } + } + + private void flush() { + flushInternal(); + try (final @NotNull ISentryLifecycleToken ignored = scheduleLock.acquire()) { + if (!queue.isEmpty()) { + maybeSchedule(true, false); + } else { + hasScheduled = false; + } + } + } + + private void flushInternal() { + do { + flushBatch(); + } while (queue.size() >= MAX_BATCH_SIZE); + } + + private void flushBatch() { + final @NotNull List metricsEvents = new ArrayList<>(MAX_BATCH_SIZE); + do { + final @Nullable SentryMetricsEvent metricsEvent = queue.poll(); + if (metricsEvent != null) { + metricsEvents.add(metricsEvent); + } + } while (!queue.isEmpty() && metricsEvents.size() < MAX_BATCH_SIZE); + + if (!metricsEvents.isEmpty()) { + client.captureBatchedMetricsEvents(new SentryMetricsEvents(metricsEvents)); + for (int i = 0; i < metricsEvents.size(); i++) { + pendingCount.decrement(); + } + } + } + + private class BatchRunnable implements Runnable { + + @Override + public void run() { + flush(); + } + } +} diff --git a/sentry/src/main/java/io/sentry/metrics/NoOpMetricsBatchProcessor.java b/sentry/src/main/java/io/sentry/metrics/NoOpMetricsBatchProcessor.java new file mode 100644 index 0000000000..3d963fb061 --- /dev/null +++ b/sentry/src/main/java/io/sentry/metrics/NoOpMetricsBatchProcessor.java @@ -0,0 +1,30 @@ +package io.sentry.metrics; + +import io.sentry.SentryMetricsEvent; +import org.jetbrains.annotations.NotNull; + +public final class NoOpMetricsBatchProcessor implements IMetricsBatchProcessor { + + private static final NoOpMetricsBatchProcessor instance = new NoOpMetricsBatchProcessor(); + + private NoOpMetricsBatchProcessor() {} + + public static NoOpMetricsBatchProcessor getInstance() { + return instance; + } + + @Override + public void add(@NotNull SentryMetricsEvent event) { + // do nothing + } + + @Override + public void close(final boolean isRestarting) { + // do nothing + } + + @Override + public void flush(long timeoutMillis) { + // do nothing + } +} From 4605172dcb98e1f751bba9405d807b1cdc629e6b Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 18 Dec 2025 13:55:10 +0100 Subject: [PATCH 07/40] Wire up Metrics parts --- .../core/SessionTrackingIntegrationTest.kt | 5 + sentry/api/sentry.api | 7 +- .../main/java/io/sentry/EventProcessor.java | 11 +++ .../main/java/io/sentry/ISentryClient.java | 2 + .../main/java/io/sentry/NoOpSentryClient.java | 5 + .../src/main/java/io/sentry/SentryClient.java | 96 +++++++++++++++++++ .../main/java/io/sentry/SentryOptions.java | 18 +++- 7 files changed, 142 insertions(+), 2 deletions(-) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt index b4235aa6b9..1a2f0db30c 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt @@ -18,6 +18,7 @@ import io.sentry.SentryEnvelope import io.sentry.SentryEvent import io.sentry.SentryLogEvent import io.sentry.SentryLogEvents +import io.sentry.SentryMetricsEvent import io.sentry.SentryMetricsEvents import io.sentry.SentryReplayEvent import io.sentry.Session @@ -193,6 +194,10 @@ class SessionTrackingIntegrationTest { TODO("Not yet implemented") } + override fun captureMetric(event: SentryMetricsEvent, scope: IScope?) { + TODO("Not yet implemented") + } + override fun captureBatchedMetricsEvents(metricsEvents: SentryMetricsEvents) { TODO("Not yet implemented") } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 3f5a20a763..7d218bc480 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -461,6 +461,7 @@ public abstract interface class io/sentry/EventProcessor { public fun getOrder ()Ljava/lang/Long; public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; public fun process (Lio/sentry/SentryLogEvent;)Lio/sentry/SentryLogEvent; + public fun process (Lio/sentry/SentryMetricsEvent;)Lio/sentry/SentryMetricsEvent; public fun process (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/SentryReplayEvent; public fun process (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction; } @@ -1045,6 +1046,7 @@ public abstract interface class io/sentry/ISentryClient { public abstract fun captureLog (Lio/sentry/SentryLogEvent;Lio/sentry/IScope;)V public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; + public abstract fun captureMetric (Lio/sentry/SentryMetricsEvent;Lio/sentry/IScope;)V public abstract fun captureProfileChunk (Lio/sentry/ProfileChunk;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; public abstract fun captureReplayEvent (Lio/sentry/SentryReplayEvent;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureSession (Lio/sentry/Session;)V @@ -2859,6 +2861,7 @@ public final class io/sentry/SentryClient : io/sentry/ISentryClient { public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureFeedback (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; public fun captureLog (Lio/sentry/SentryLogEvent;Lio/sentry/IScope;)V + public fun captureMetric (Lio/sentry/SentryMetricsEvent;Lio/sentry/IScope;)V public fun captureProfileChunk (Lio/sentry/ProfileChunk;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; public fun captureReplayEvent (Lio/sentry/SentryReplayEvent;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureSession (Lio/sentry/Session;Lio/sentry/Hint;)V @@ -3755,13 +3758,15 @@ public abstract interface class io/sentry/SentryOptions$Logs$BeforeSendLogCallba public final class io/sentry/SentryOptions$Metrics { public fun ()V public fun getBeforeSend ()Lio/sentry/SentryOptions$Metrics$BeforeSendMetricCallback; + public fun getMetricsBatchProcessorFactory ()Lio/sentry/metrics/IMetricsBatchProcessorFactory; public fun isEnabled ()Z public fun setBeforeSend (Lio/sentry/SentryOptions$Metrics$BeforeSendMetricCallback;)V public fun setEnabled (Z)V + public fun setMetricsBatchProcessorFactory (Lio/sentry/metrics/IMetricsBatchProcessorFactory;)V } public abstract interface class io/sentry/SentryOptions$Metrics$BeforeSendMetricCallback { - public abstract fun execute (Lio/sentry/SentryMetricsEvents;)Lio/sentry/SentryMetricsEvents; + public abstract fun execute (Lio/sentry/SentryMetricsEvent;)Lio/sentry/SentryMetricsEvent; } public abstract interface class io/sentry/SentryOptions$OnDiscardCallback { diff --git a/sentry/src/main/java/io/sentry/EventProcessor.java b/sentry/src/main/java/io/sentry/EventProcessor.java index b258132edf..4f59de7578 100644 --- a/sentry/src/main/java/io/sentry/EventProcessor.java +++ b/sentry/src/main/java/io/sentry/EventProcessor.java @@ -56,6 +56,17 @@ default SentryLogEvent process(@NotNull SentryLogEvent event) { return event; } + /** + * May mutate or drop a SentryMetricsEvent + * + * @param event the SentryMetricsEvent + * @return the event itself, a mutated SentryMetricsEvent or null + */ + @Nullable + default SentryMetricsEvent process(@NotNull SentryMetricsEvent event) { + return event; + } + /** * Controls when this EventProcessor is invoked. * diff --git a/sentry/src/main/java/io/sentry/ISentryClient.java b/sentry/src/main/java/io/sentry/ISentryClient.java index cd9ce85e9a..79172be060 100644 --- a/sentry/src/main/java/io/sentry/ISentryClient.java +++ b/sentry/src/main/java/io/sentry/ISentryClient.java @@ -305,6 +305,8 @@ SentryId captureProfileChunk( void captureLog(@NotNull SentryLogEvent logEvent, @Nullable IScope scope); + void captureMetric(@NotNull SentryMetricsEvent logEvent, @Nullable IScope scope); + @ApiStatus.Internal void captureBatchedLogEvents(@NotNull SentryLogEvents logEvents); diff --git a/sentry/src/main/java/io/sentry/NoOpSentryClient.java b/sentry/src/main/java/io/sentry/NoOpSentryClient.java index cd2ed885ca..47b3343874 100644 --- a/sentry/src/main/java/io/sentry/NoOpSentryClient.java +++ b/sentry/src/main/java/io/sentry/NoOpSentryClient.java @@ -88,6 +88,11 @@ public void captureLog(@NotNull SentryLogEvent logEvent, @Nullable IScope scope) // do nothing } + @Override + public void captureMetric(@NotNull SentryMetricsEvent metricsEvent, @Nullable IScope scope) { + // do nothing + } + @ApiStatus.Internal @Override public void captureBatchedLogEvents(@NotNull SentryLogEvents logEvents) { diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index ed6c5cbe03..e67a6a6aea 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -10,6 +10,8 @@ import io.sentry.hints.TransactionEnd; import io.sentry.logger.ILoggerBatchProcessor; import io.sentry.logger.NoOpLoggerBatchProcessor; +import io.sentry.metrics.IMetricsBatchProcessor; +import io.sentry.metrics.NoOpMetricsBatchProcessor; import io.sentry.protocol.Contexts; import io.sentry.protocol.DebugMeta; import io.sentry.protocol.FeatureFlags; @@ -42,6 +44,7 @@ public final class SentryClient implements ISentryClient { private final @NotNull ITransport transport; private final @NotNull SortBreadcrumbsByDate sortBreadcrumbsByDate = new SortBreadcrumbsByDate(); private final @NotNull ILoggerBatchProcessor loggerBatchProcessor; + private final @NotNull IMetricsBatchProcessor metricsBatchProcessor; @Override public boolean isEnabled() { @@ -66,6 +69,12 @@ public SentryClient(final @NotNull SentryOptions options) { } else { loggerBatchProcessor = NoOpLoggerBatchProcessor.getInstance(); } + if (options.getMetrics().isEnabled()) { + metricsBatchProcessor = + options.getMetrics().getMetricsBatchProcessorFactory().create(options, this); + } else { + metricsBatchProcessor = NoOpMetricsBatchProcessor.getInstance(); + } } private boolean shouldApplyScopeData( @@ -506,6 +515,38 @@ private SentryLogEvent processLogEvent( return event; } + @Nullable + private SentryMetricsEvent processMetricsEvent( + @NotNull SentryMetricsEvent event, final @NotNull List eventProcessors) { + for (final EventProcessor processor : eventProcessors) { + try { + event = processor.process(event); + } catch (Throwable e) { + options + .getLogger() + .log( + SentryLevel.ERROR, + e, + "An exception occurred while processing metrics event by processor: %s", + processor.getClass().getName()); + } + + if (event == null) { + options + .getLogger() + .log( + SentryLevel.DEBUG, + "Metrics event was dropped by a processor: %s", + processor.getClass().getName()); + options + .getClientReportRecorder() + .recordLostEvent(DiscardReason.EVENT_PROCESSOR, DataCategory.TraceMetric); + break; + } + } + return event; + } + private @Nullable SentryTransaction processTransaction( @NotNull SentryTransaction transaction, final @NotNull Hint hint, @@ -1235,6 +1276,40 @@ public void captureBatchedLogEvents(final @NotNull SentryLogEvents logEvents) { } } + @ApiStatus.Experimental + @Override + public void captureMetric(@Nullable SentryMetricsEvent metricsEvent, @Nullable IScope scope) { + if (metricsEvent != null && scope != null) { + metricsEvent = processMetricsEvent(metricsEvent, scope.getEventProcessors()); + if (metricsEvent == null) { + return; + } + } + + if (metricsEvent != null) { + metricsEvent = processMetricsEvent(metricsEvent, options.getEventProcessors()); + if (metricsEvent == null) { + return; + } + } + + if (metricsEvent != null) { + metricsEvent = executeBeforeSendMetric(metricsEvent); + + if (metricsEvent == null) { + options + .getLogger() + .log(SentryLevel.DEBUG, "Metrics Event was dropped by beforeSendMetrics"); + options + .getClientReportRecorder() + .recordLostEvent(DiscardReason.BEFORE_SEND, DataCategory.TraceMetric); + return; + } + + metricsBatchProcessor.add(metricsEvent); + } + } + @ApiStatus.Internal @Override public void captureBatchedMetricsEvents(final @NotNull SentryMetricsEvents metricsEvents) { @@ -1550,6 +1625,27 @@ private void sortBreadcrumbsByDate( return event; } + private @Nullable SentryMetricsEvent executeBeforeSendMetric(@NotNull SentryMetricsEvent event) { + final SentryOptions.Metrics.BeforeSendMetricCallback beforeSendMetric = + options.getMetrics().getBeforeSend(); + if (beforeSendMetric != null) { + try { + event = beforeSendMetric.execute(event); + } catch (Throwable e) { + options + .getLogger() + .log( + SentryLevel.ERROR, + "The BeforeSendMetric callback threw an exception. Dropping metrics event.", + e); + + // drop event in case of an error in beforeSendMetric due to PII concerns + event = null; + } + } + return event; + } + @Override public void close() { close(false); diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index e832a30726..724ee727ef 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -17,6 +17,8 @@ import io.sentry.internal.viewhierarchy.ViewHierarchyExporter; import io.sentry.logger.DefaultLoggerBatchProcessorFactory; import io.sentry.logger.ILoggerBatchProcessorFactory; +import io.sentry.metrics.DefaultMetricsBatchProcessorFactory; +import io.sentry.metrics.IMetricsBatchProcessorFactory; import io.sentry.protocol.SdkVersion; import io.sentry.protocol.SentryTransaction; import io.sentry.transport.ITransport; @@ -3750,6 +3752,9 @@ public static final class Metrics { */ private @Nullable BeforeSendMetricCallback beforeSend; + private @NotNull IMetricsBatchProcessorFactory metricsBatchProcessorFactory = + new DefaultMetricsBatchProcessorFactory(); + /** * Whether Sentry Metrics feature is enabled and metrics are sent to Sentry. * @@ -3786,6 +3791,17 @@ public void setBeforeSend(@Nullable BeforeSendMetricCallback beforeSend) { this.beforeSend = beforeSend; } + @ApiStatus.Internal + public @NotNull IMetricsBatchProcessorFactory getMetricsBatchProcessorFactory() { + return metricsBatchProcessorFactory; + } + + @ApiStatus.Internal + public void setMetricsBatchProcessorFactory( + final @NotNull IMetricsBatchProcessorFactory metricsBatchProcessorFactory) { + this.metricsBatchProcessorFactory = metricsBatchProcessorFactory; + } + public interface BeforeSendMetricCallback { /** @@ -3795,7 +3811,7 @@ public interface BeforeSendMetricCallback { * @return the original metric, mutated metric or null if metric was dropped */ @Nullable - SentryMetricsEvents execute(@NotNull SentryMetricsEvents metric); + SentryMetricsEvent execute(@NotNull SentryMetricsEvent metric); } } From 38a5226576c4d7d7a280d9cd3f0fdb7404d0370c Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 18 Dec 2025 13:55:44 +0100 Subject: [PATCH 08/40] Metrics counter basic API --- .../java/io/sentry/metrics/MetricsApi.java | 213 +++++++++++++++++- 1 file changed, 212 insertions(+), 1 deletion(-) diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java index 7b878de3d2..24ea873dda 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java @@ -1,7 +1,28 @@ package io.sentry.metrics; +import io.sentry.HostnameCache; +import io.sentry.IScope; +import io.sentry.ISpan; +import io.sentry.PropagationContext; import io.sentry.Scopes; +import io.sentry.SentryAttribute; +import io.sentry.SentryAttributeType; +import io.sentry.SentryAttributes; +import io.sentry.SentryDate; +import io.sentry.SentryLevel; +import io.sentry.SentryLogEventAttributeValue; +import io.sentry.SentryMetricsEvent; +import io.sentry.SentryOptions; +import io.sentry.SpanId; +import io.sentry.logger.SentryLogParameters; +import io.sentry.protocol.SdkVersion; +import io.sentry.protocol.SentryId; +import io.sentry.protocol.User; +import io.sentry.util.Platform; +import io.sentry.util.TracingUtils; +import java.util.HashMap; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public final class MetricsApi implements IMetricsApi { @@ -13,6 +34,196 @@ public MetricsApi(final @NotNull Scopes scopes) { @Override public void count(@NotNull String name) { - scopes.getOptions(); + captureMetrics(SentryLogParameters.create(null, null), name, "counter", 1.0); + } + + @SuppressWarnings("AnnotateFormatMethod") + private void captureMetrics( + final @NotNull SentryLogParameters params, + final @Nullable String name, + final @Nullable String type, + final @Nullable Double value) { + final @NotNull SentryOptions options = scopes.getOptions(); + try { + if (!scopes.isEnabled()) { + options + .getLogger() + .log(SentryLevel.WARNING, "Instance is disabled and this 'metrics' call is a no-op."); + return; + } + + if (!options.getMetrics().isEnabled()) { + options + .getLogger() + .log( + SentryLevel.WARNING, + "Sentry Metrics is disabled and this 'metrics' call is a no-op."); + return; + } + + if (name == null) { + return; + } + + if (type == null) { + return; + } + + if (value == null) { + return; + } + + final @Nullable SentryDate timestamp = params.getTimestamp(); + final @NotNull SentryDate timestampToUse = + timestamp == null ? options.getDateProvider().now() : timestamp; + + final @NotNull IScope combinedScope = scopes.getCombinedScopeView(); + final @NotNull PropagationContext propagationContext = combinedScope.getPropagationContext(); + final @Nullable ISpan span = combinedScope.getSpan(); + if (span == null) { + TracingUtils.maybeUpdateBaggage(combinedScope, options); + } + final @NotNull SentryId traceId = + span == null ? propagationContext.getTraceId() : span.getSpanContext().getTraceId(); + final @NotNull SpanId spanId = + span == null ? propagationContext.getSpanId() : span.getSpanContext().getSpanId(); + final SentryMetricsEvent metricsEvent = + new SentryMetricsEvent(traceId, timestampToUse, name, type, value); + metricsEvent.setSpanId(spanId); + metricsEvent.setAttributes(createAttributes(params)); + + scopes.getClient().captureMetric(metricsEvent, combinedScope); + } catch (Throwable e) { + options.getLogger().log(SentryLevel.ERROR, "Error while capturing log event", e); + } + } + + private @NotNull HashMap createAttributes( + final @NotNull SentryLogParameters params) { + final @NotNull HashMap attributes = new HashMap<>(); + final @NotNull String origin = params.getOrigin(); + if (!"manual".equalsIgnoreCase(origin)) { + attributes.put( + "sentry.origin", new SentryLogEventAttributeValue(SentryAttributeType.STRING, origin)); + } + + final @Nullable SentryAttributes incomingAttributes = params.getAttributes(); + + 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)); + } + } + + final @Nullable SdkVersion sdkVersion = scopes.getOptions().getSdkVersion(); + if (sdkVersion != null) { + attributes.put( + "sentry.sdk.name", + new SentryLogEventAttributeValue(SentryAttributeType.STRING, sdkVersion.getName())); + attributes.put( + "sentry.sdk.version", + new SentryLogEventAttributeValue(SentryAttributeType.STRING, sdkVersion.getVersion())); + } + + final @Nullable String environment = scopes.getOptions().getEnvironment(); + if (environment != null) { + attributes.put( + "sentry.environment", + new SentryLogEventAttributeValue(SentryAttributeType.STRING, environment)); + } + + final @NotNull SentryId scopeReplayId = scopes.getCombinedScopeView().getReplayId(); + if (!SentryId.EMPTY_ID.equals(scopeReplayId)) { + attributes.put( + "sentry.replay_id", + new SentryLogEventAttributeValue(SentryAttributeType.STRING, scopeReplayId.toString())); + } else { + final @NotNull SentryId controllerReplayId = + scopes.getOptions().getReplayController().getReplayId(); + if (!SentryId.EMPTY_ID.equals(controllerReplayId)) { + attributes.put( + "sentry.replay_id", + new SentryLogEventAttributeValue( + SentryAttributeType.STRING, controllerReplayId.toString())); + attributes.put( + "sentry._internal.replay_is_buffering", + new SentryLogEventAttributeValue(SentryAttributeType.BOOLEAN, true)); + } + } + + final @Nullable String release = scopes.getOptions().getRelease(); + if (release != null) { + attributes.put( + "sentry.release", new SentryLogEventAttributeValue(SentryAttributeType.STRING, release)); + } + + if (Platform.isJvm()) { + setServerName(attributes); + } + + setUser(attributes); + + return attributes; + } + + private void setServerName( + final @NotNull HashMap attributes) { + final @NotNull SentryOptions options = scopes.getOptions(); + final @Nullable String optionsServerName = options.getServerName(); + if (optionsServerName != null) { + attributes.put( + "server.address", + new SentryLogEventAttributeValue(SentryAttributeType.STRING, optionsServerName)); + } else if (options.isAttachServerName()) { + final @Nullable String hostname = HostnameCache.getInstance().getHostname(); + if (hostname != null) { + attributes.put( + "server.address", + new SentryLogEventAttributeValue(SentryAttributeType.STRING, hostname)); + } + } + } + + private void setUser(final @NotNull HashMap attributes) { + final @Nullable User user = scopes.getCombinedScopeView().getUser(); + if (user == null) { + // In case no user is set, we should fallback to the distinct id, known as installation id, + // which is used on Android as default user id + final @Nullable String id = scopes.getOptions().getDistinctId(); + if (id != null) { + attributes.put("user.id", new SentryLogEventAttributeValue(SentryAttributeType.STRING, id)); + } + } else { + final @Nullable String id = user.getId(); + if (id != null) { + attributes.put("user.id", new SentryLogEventAttributeValue(SentryAttributeType.STRING, id)); + } + final @Nullable String username = user.getUsername(); + if (username != null) { + attributes.put( + "user.name", new SentryLogEventAttributeValue(SentryAttributeType.STRING, username)); + } + final @Nullable String email = user.getEmail(); + if (email != null) { + attributes.put( + "user.email", new SentryLogEventAttributeValue(SentryAttributeType.STRING, email)); + } + } + } + + private @NotNull SentryAttributeType getType(final @Nullable Object arg) { + if (arg instanceof Boolean) { + return SentryAttributeType.BOOLEAN; + } + if (arg instanceof Integer) { + return SentryAttributeType.INTEGER; + } + if (arg instanceof Number) { + return SentryAttributeType.DOUBLE; + } + return SentryAttributeType.STRING; } } From 557f6dd5fb2f35936ebf471f9b1c1af243bf7191 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 18 Dec 2025 14:30:00 +0100 Subject: [PATCH 09/40] Metrics Count API --- sentry/api/sentry.api | 12 +++++++ .../java/io/sentry/metrics/IMetricsApi.java | 17 +++++++++- .../java/io/sentry/metrics/MetricsApi.java | 33 +++++++++++++++++-- .../io/sentry/metrics/NoOpMetricsApi.java | 21 +++++++++++- 4 files changed, 78 insertions(+), 5 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 7d218bc480..38a85611ad 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -5206,6 +5206,10 @@ public final class io/sentry/metrics/DefaultMetricsBatchProcessorFactory : io/se public abstract interface class io/sentry/metrics/IMetricsApi { public abstract fun count (Ljava/lang/String;)V + public abstract fun count (Ljava/lang/String;Ljava/lang/Double;)V + public abstract fun count (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;)V + public abstract fun count (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/logger/SentryLogParameters;)V + public abstract fun count (Ljava/lang/String;Ljava/lang/String;)V } public abstract interface class io/sentry/metrics/IMetricsBatchProcessor { @@ -5221,6 +5225,10 @@ public abstract interface class io/sentry/metrics/IMetricsBatchProcessorFactory public final class io/sentry/metrics/MetricsApi : io/sentry/metrics/IMetricsApi { public fun (Lio/sentry/Scopes;)V public fun count (Ljava/lang/String;)V + public fun count (Ljava/lang/String;Ljava/lang/Double;)V + public fun count (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;)V + public fun count (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/logger/SentryLogParameters;)V + public fun count (Ljava/lang/String;Ljava/lang/String;)V } public class io/sentry/metrics/MetricsBatchProcessor : io/sentry/metrics/IMetricsBatchProcessor { @@ -5236,6 +5244,10 @@ public class io/sentry/metrics/MetricsBatchProcessor : io/sentry/metrics/IMetric public final class io/sentry/metrics/NoOpMetricsApi : io/sentry/metrics/IMetricsApi { public fun count (Ljava/lang/String;)V + public fun count (Ljava/lang/String;Ljava/lang/Double;)V + public fun count (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;)V + public fun count (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/logger/SentryLogParameters;)V + public fun count (Ljava/lang/String;Ljava/lang/String;)V public static fun getInstance ()Lio/sentry/metrics/NoOpMetricsApi; } diff --git a/sentry/src/main/java/io/sentry/metrics/IMetricsApi.java b/sentry/src/main/java/io/sentry/metrics/IMetricsApi.java index ddd6bb9d41..afaf8ea3cf 100644 --- a/sentry/src/main/java/io/sentry/metrics/IMetricsApi.java +++ b/sentry/src/main/java/io/sentry/metrics/IMetricsApi.java @@ -1,10 +1,25 @@ package io.sentry.metrics; +import io.sentry.logger.SentryLogParameters; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public interface IMetricsApi { - void count(@NotNull final String name); + void count(final @NotNull String name); + + void count(final @NotNull String name, final @Nullable Double value); + + void count(final @NotNull String name, final @Nullable String unit); + + void count(final @NotNull String name, final @Nullable Double value, final @Nullable String unit); + + void count( + final @NotNull String name, + final @Nullable Double value, + final @Nullable String unit, + final @NotNull SentryLogParameters params); + // distribution // gauge // diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java index 24ea873dda..1d18fa064f 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java @@ -33,8 +33,33 @@ public MetricsApi(final @NotNull Scopes scopes) { } @Override - public void count(@NotNull String name) { - captureMetrics(SentryLogParameters.create(null, null), name, "counter", 1.0); + public void count(final @NotNull String name) { + captureMetrics(SentryLogParameters.create(null, null), name, "counter", 1.0, null); + } + + @Override + public void count(final @NotNull String name, final @Nullable Double value) { + captureMetrics(SentryLogParameters.create(null, null), name, "counter", value, null); + } + + @Override + public void count(final @NotNull String name, final @Nullable String unit) { + captureMetrics(SentryLogParameters.create(null, null), name, "counter", 1.0, unit); + } + + @Override + public void count( + final @NotNull String name, final @Nullable Double value, final @Nullable String unit) { + captureMetrics(SentryLogParameters.create(null, null), name, "counter", value, unit); + } + + @Override + public void count( + final @NotNull String name, + final @Nullable Double value, + final @Nullable String unit, + final @NotNull SentryLogParameters params) { + captureMetrics(params, name, "counter", value, unit); } @SuppressWarnings("AnnotateFormatMethod") @@ -42,7 +67,8 @@ private void captureMetrics( final @NotNull SentryLogParameters params, final @Nullable String name, final @Nullable String type, - final @Nullable Double value) { + final @Nullable Double value, + final @Nullable String unit) { final @NotNull SentryOptions options = scopes.getOptions(); try { if (!scopes.isEnabled()) { @@ -90,6 +116,7 @@ private void captureMetrics( final SentryMetricsEvent metricsEvent = new SentryMetricsEvent(traceId, timestampToUse, name, type, value); metricsEvent.setSpanId(spanId); + metricsEvent.setUnit(unit); metricsEvent.setAttributes(createAttributes(params)); scopes.getClient().captureMetric(metricsEvent, combinedScope); diff --git a/sentry/src/main/java/io/sentry/metrics/NoOpMetricsApi.java b/sentry/src/main/java/io/sentry/metrics/NoOpMetricsApi.java index f6977b6953..b12b863167 100644 --- a/sentry/src/main/java/io/sentry/metrics/NoOpMetricsApi.java +++ b/sentry/src/main/java/io/sentry/metrics/NoOpMetricsApi.java @@ -1,6 +1,8 @@ package io.sentry.metrics; +import io.sentry.logger.SentryLogParameters; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; public final class NoOpMetricsApi implements IMetricsApi { private static final NoOpMetricsApi instance = new NoOpMetricsApi(); @@ -12,5 +14,22 @@ public static NoOpMetricsApi getInstance() { } @Override - public void count(@NotNull String name) {} + public void count(final @NotNull String name) {} + + @Override + public void count(final @NotNull String name, final @Nullable Double value) {} + + @Override + public void count(final @NotNull String name, final @Nullable String unit) {} + + @Override + public void count( + final @NotNull String name, final @Nullable Double value, final @Nullable String unit) {} + + @Override + public void count( + final @NotNull String name, + final @Nullable Double value, + final @Nullable String unit, + final @NotNull SentryLogParameters params) {} } From 661415a439e0f97cf57670d72eb9b77085766fe9 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 18 Dec 2025 15:03:46 +0100 Subject: [PATCH 10/40] Metrics Distribution API --- sentry/api/sentry.api | 9 +++++++++ .../java/io/sentry/metrics/IMetricsApi.java | 12 ++++++++++- .../java/io/sentry/metrics/MetricsApi.java | 20 +++++++++++++++++++ .../io/sentry/metrics/NoOpMetricsApi.java | 14 +++++++++++++ 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 38a85611ad..705cd7f8cb 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -5210,6 +5210,9 @@ public abstract interface class io/sentry/metrics/IMetricsApi { public abstract fun count (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;)V public abstract fun count (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/logger/SentryLogParameters;)V public abstract fun count (Ljava/lang/String;Ljava/lang/String;)V + public abstract fun distribution (Ljava/lang/String;Ljava/lang/Double;)V + public abstract fun distribution (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;)V + public abstract fun distribution (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/logger/SentryLogParameters;)V } public abstract interface class io/sentry/metrics/IMetricsBatchProcessor { @@ -5229,6 +5232,9 @@ public final class io/sentry/metrics/MetricsApi : io/sentry/metrics/IMetricsApi public fun count (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;)V public fun count (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/logger/SentryLogParameters;)V public fun count (Ljava/lang/String;Ljava/lang/String;)V + public fun distribution (Ljava/lang/String;Ljava/lang/Double;)V + public fun distribution (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;)V + public fun distribution (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/logger/SentryLogParameters;)V } public class io/sentry/metrics/MetricsBatchProcessor : io/sentry/metrics/IMetricsBatchProcessor { @@ -5248,6 +5254,9 @@ public final class io/sentry/metrics/NoOpMetricsApi : io/sentry/metrics/IMetrics public fun count (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;)V public fun count (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/logger/SentryLogParameters;)V public fun count (Ljava/lang/String;Ljava/lang/String;)V + public fun distribution (Ljava/lang/String;Ljava/lang/Double;)V + public fun distribution (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;)V + public fun distribution (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/logger/SentryLogParameters;)V public static fun getInstance ()Lio/sentry/metrics/NoOpMetricsApi; } diff --git a/sentry/src/main/java/io/sentry/metrics/IMetricsApi.java b/sentry/src/main/java/io/sentry/metrics/IMetricsApi.java index afaf8ea3cf..6d4d472598 100644 --- a/sentry/src/main/java/io/sentry/metrics/IMetricsApi.java +++ b/sentry/src/main/java/io/sentry/metrics/IMetricsApi.java @@ -20,7 +20,17 @@ void count( final @Nullable String unit, final @NotNull SentryLogParameters params); - // distribution + void distribution(final @NotNull String name, final @Nullable Double value); + + void distribution( + final @NotNull String name, final @Nullable Double value, final @Nullable String unit); + + void distribution( + final @NotNull String name, + final @Nullable Double value, + final @Nullable String unit, + final @NotNull SentryLogParameters params); + // gauge // } diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java index 1d18fa064f..b8233f99bb 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java @@ -62,6 +62,26 @@ public void count( captureMetrics(params, name, "counter", value, unit); } + @Override + public void distribution(final @NotNull String name, final @Nullable Double value) { + captureMetrics(SentryLogParameters.create(null, null), name, "distribution", value, null); + } + + @Override + public void distribution( + final @NotNull String name, final @Nullable Double value, final @Nullable String unit) { + captureMetrics(SentryLogParameters.create(null, null), name, "distribution", value, unit); + } + + @Override + public void distribution( + final @NotNull String name, + final @Nullable Double value, + final @Nullable String unit, + final @NotNull SentryLogParameters params) { + captureMetrics(params, name, "distribution", value, unit); + } + @SuppressWarnings("AnnotateFormatMethod") private void captureMetrics( final @NotNull SentryLogParameters params, diff --git a/sentry/src/main/java/io/sentry/metrics/NoOpMetricsApi.java b/sentry/src/main/java/io/sentry/metrics/NoOpMetricsApi.java index b12b863167..7cfcb9e61f 100644 --- a/sentry/src/main/java/io/sentry/metrics/NoOpMetricsApi.java +++ b/sentry/src/main/java/io/sentry/metrics/NoOpMetricsApi.java @@ -32,4 +32,18 @@ public void count( final @Nullable Double value, final @Nullable String unit, final @NotNull SentryLogParameters params) {} + + @Override + public void distribution(final @NotNull String name, final @Nullable Double value) {} + + @Override + public void distribution( + final @NotNull String name, final @Nullable Double value, final @Nullable String unit) {} + + @Override + public void distribution( + final @NotNull String name, + final @Nullable Double value, + final @Nullable String unit, + final @NotNull SentryLogParameters params) {} } From ece88f1d1ff1e0950c17dd61a9d2a8a7c33be149 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 19 Dec 2025 09:41:31 +0100 Subject: [PATCH 11/40] Metrics Gauge API --- sentry/api/sentry.api | 9 +++++++++ .../java/io/sentry/metrics/IMetricsApi.java | 11 ++++++++-- .../java/io/sentry/metrics/MetricsApi.java | 20 +++++++++++++++++++ .../io/sentry/metrics/NoOpMetricsApi.java | 14 +++++++++++++ 4 files changed, 52 insertions(+), 2 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 705cd7f8cb..6e6c7ab94b 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -5213,6 +5213,9 @@ public abstract interface class io/sentry/metrics/IMetricsApi { public abstract fun distribution (Ljava/lang/String;Ljava/lang/Double;)V public abstract fun distribution (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;)V public abstract fun distribution (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/logger/SentryLogParameters;)V + public abstract fun gauge (Ljava/lang/String;Ljava/lang/Double;)V + public abstract fun gauge (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;)V + public abstract fun gauge (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/logger/SentryLogParameters;)V } public abstract interface class io/sentry/metrics/IMetricsBatchProcessor { @@ -5235,6 +5238,9 @@ public final class io/sentry/metrics/MetricsApi : io/sentry/metrics/IMetricsApi public fun distribution (Ljava/lang/String;Ljava/lang/Double;)V public fun distribution (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;)V public fun distribution (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/logger/SentryLogParameters;)V + public fun gauge (Ljava/lang/String;Ljava/lang/Double;)V + public fun gauge (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;)V + public fun gauge (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/logger/SentryLogParameters;)V } public class io/sentry/metrics/MetricsBatchProcessor : io/sentry/metrics/IMetricsBatchProcessor { @@ -5257,6 +5263,9 @@ public final class io/sentry/metrics/NoOpMetricsApi : io/sentry/metrics/IMetrics public fun distribution (Ljava/lang/String;Ljava/lang/Double;)V public fun distribution (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;)V public fun distribution (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/logger/SentryLogParameters;)V + public fun gauge (Ljava/lang/String;Ljava/lang/Double;)V + public fun gauge (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;)V + public fun gauge (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/logger/SentryLogParameters;)V public static fun getInstance ()Lio/sentry/metrics/NoOpMetricsApi; } diff --git a/sentry/src/main/java/io/sentry/metrics/IMetricsApi.java b/sentry/src/main/java/io/sentry/metrics/IMetricsApi.java index 6d4d472598..e61ec3dc21 100644 --- a/sentry/src/main/java/io/sentry/metrics/IMetricsApi.java +++ b/sentry/src/main/java/io/sentry/metrics/IMetricsApi.java @@ -31,6 +31,13 @@ void distribution( final @Nullable String unit, final @NotNull SentryLogParameters params); - // gauge - // + void gauge(final @NotNull String name, final @Nullable Double value); + + void gauge(final @NotNull String name, final @Nullable Double value, final @Nullable String unit); + + void gauge( + final @NotNull String name, + final @Nullable Double value, + final @Nullable String unit, + final @NotNull SentryLogParameters params); } diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java index b8233f99bb..71ec6118ee 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java @@ -82,6 +82,26 @@ public void distribution( captureMetrics(params, name, "distribution", value, unit); } + @Override + public void gauge(final @NotNull String name, final @Nullable Double value) { + captureMetrics(SentryLogParameters.create(null, null), name, "gauge", value, null); + } + + @Override + public void gauge( + final @NotNull String name, final @Nullable Double value, final @Nullable String unit) { + captureMetrics(SentryLogParameters.create(null, null), name, "gauge", value, unit); + } + + @Override + public void gauge( + final @NotNull String name, + final @Nullable Double value, + final @Nullable String unit, + final @NotNull SentryLogParameters params) { + captureMetrics(params, name, "gauge", value, unit); + } + @SuppressWarnings("AnnotateFormatMethod") private void captureMetrics( final @NotNull SentryLogParameters params, diff --git a/sentry/src/main/java/io/sentry/metrics/NoOpMetricsApi.java b/sentry/src/main/java/io/sentry/metrics/NoOpMetricsApi.java index 7cfcb9e61f..6e770b249c 100644 --- a/sentry/src/main/java/io/sentry/metrics/NoOpMetricsApi.java +++ b/sentry/src/main/java/io/sentry/metrics/NoOpMetricsApi.java @@ -46,4 +46,18 @@ public void distribution( final @Nullable Double value, final @Nullable String unit, final @NotNull SentryLogParameters params) {} + + @Override + public void gauge(final @NotNull String name, final @Nullable Double value) {} + + @Override + public void gauge( + final @NotNull String name, final @Nullable Double value, final @Nullable String unit) {} + + @Override + public void gauge( + final @NotNull String name, + final @Nullable Double value, + final @Nullable String unit, + final @NotNull SentryLogParameters params) {} } From 5aed561304a79b3edbb30d1b013d78bbf9796555 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 19 Dec 2025 13:25:29 +0100 Subject: [PATCH 12/40] Metrics Envelope item deserialization --- sentry/api/sentry.api | 1 + sentry/src/main/java/io/sentry/JsonSerializer.java | 1 + .../src/main/java/io/sentry/SentryEnvelopeItem.java | 11 +++++++++++ 3 files changed, 13 insertions(+) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 6e6c7ab94b..9b9881d9f9 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2955,6 +2955,7 @@ public final class io/sentry/SentryEnvelopeItem { public fun getEvent (Lio/sentry/ISerializer;)Lio/sentry/SentryEvent; public fun getHeader ()Lio/sentry/SentryEnvelopeItemHeader; public fun getLogs (Lio/sentry/ISerializer;)Lio/sentry/SentryLogEvents; + public fun getMetrics (Lio/sentry/ISerializer;)Lio/sentry/SentryMetricsEvents; public fun getTransaction (Lio/sentry/ISerializer;)Lio/sentry/protocol/SentryTransaction; } diff --git a/sentry/src/main/java/io/sentry/JsonSerializer.java b/sentry/src/main/java/io/sentry/JsonSerializer.java index c643fd2a69..a0fa80879a 100644 --- a/sentry/src/main/java/io/sentry/JsonSerializer.java +++ b/sentry/src/main/java/io/sentry/JsonSerializer.java @@ -123,6 +123,7 @@ public JsonSerializer(@NotNull SentryOptions options) { deserializersByClass.put(SentryLevel.class, new SentryLevel.Deserializer()); deserializersByClass.put(SentryLockReason.class, new SentryLockReason.Deserializer()); deserializersByClass.put(SentryLogEvents.class, new SentryLogEvents.Deserializer()); + deserializersByClass.put(SentryMetricsEvents.class, new SentryMetricsEvents.Deserializer()); deserializersByClass.put(SentryPackage.class, new SentryPackage.Deserializer()); deserializersByClass.put(SentryRuntime.class, new SentryRuntime.Deserializer()); deserializersByClass.put(SentryReplayEvent.class, new SentryReplayEvent.Deserializer()); diff --git a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java index 155c40ae7c..58d150886d 100644 --- a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java +++ b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java @@ -158,6 +158,17 @@ public final class SentryEnvelopeItem { } } + public @Nullable SentryMetricsEvents getMetrics(final @NotNull ISerializer serializer) + throws Exception { + if (header == null || header.getType() != SentryItemType.TraceMetric) { + return null; + } + try (final Reader eventReader = + new BufferedReader(new InputStreamReader(new ByteArrayInputStream(getData()), UTF_8))) { + return serializer.deserialize(eventReader, SentryMetricsEvents.class); + } + } + public static SentryEnvelopeItem fromUserFeedback( final @NotNull ISerializer serializer, final @NotNull UserFeedback userFeedback) { Objects.requireNonNull(serializer, "ISerializer is required."); From 0107c8e9b4520175d4b06959eecc9b459ccba386 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 19 Dec 2025 13:26:00 +0100 Subject: [PATCH 13/40] Record client report for discarded metrics envelope item --- .../clientreport/ClientReportRecorder.java | 15 +++++++++ .../sentry/clientreport/ClientReportTest.kt | 32 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java index eccf311cf8..0180b16977 100644 --- a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java +++ b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java @@ -8,6 +8,8 @@ import io.sentry.SentryLevel; import io.sentry.SentryLogEvent; import io.sentry.SentryLogEvents; +import io.sentry.SentryMetricsEvent; +import io.sentry.SentryMetricsEvents; import io.sentry.SentryOptions; import io.sentry.protocol.SentrySpan; import io.sentry.protocol.SentryTransaction; @@ -115,6 +117,19 @@ public void recordLostEnvelopeItem( } else { options.getLogger().log(SentryLevel.ERROR, "Unable to parse lost logs envelope item."); } + } else if (itemCategory.equals(DataCategory.TraceMetric)) { + final @Nullable SentryMetricsEvents metrics = + envelopeItem.getMetrics(options.getSerializer()); + if (metrics != null) { + final @NotNull List items = metrics.getItems(); + final long count = items.size(); + recordLostEventInternal(reason.getReason(), itemCategory.getCategory(), count); + executeOnDiscard(reason, itemCategory, count); + } else { + options + .getLogger() + .log(SentryLevel.ERROR, "Unable to parse lost metrics envelope item."); + } } else { recordLostEventInternal(reason.getReason(), itemCategory.getCategory(), 1L); executeOnDiscard(reason, itemCategory, 1L); diff --git a/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt b/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt index ae4a5f3536..95639d8c58 100644 --- a/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt +++ b/sentry/src/test/java/io/sentry/clientreport/ClientReportTest.kt @@ -20,6 +20,8 @@ import io.sentry.SentryLogEvent import io.sentry.SentryLogEvents import io.sentry.SentryLogLevel import io.sentry.SentryLongDate +import io.sentry.SentryMetricsEvent +import io.sentry.SentryMetricsEvents import io.sentry.SentryOptions import io.sentry.SentryReplayEvent import io.sentry.SentryTracer @@ -382,6 +384,36 @@ class ClientReportTest { assertEquals(226, logByte.quantity) } + @Test + fun `recording lost client report counts metric entries`() { + val onDiscardMock = mock() + givenClientReportRecorder { options -> options.onDiscard = onDiscardMock } + + val envelope = + testHelper.newEnvelope( + SentryEnvelopeItem.fromMetrics( + opts.serializer, + SentryMetricsEvents( + listOf( + SentryMetricsEvent(SentryId(), SentryLongDate(1), "metric1", "counter", 1.0), + SentryMetricsEvent(SentryId(), SentryLongDate(2), "metric2", "gauge", 2.0), + SentryMetricsEvent(SentryId(), SentryLongDate(3), "metric3", "distribution", 3.0), + ) + ), + ) + ) + + clientReportRecorder.recordLostEnvelopeItem(DiscardReason.NETWORK_ERROR, envelope.items.first()) + + verify(onDiscardMock, times(1)) + .execute(DiscardReason.NETWORK_ERROR, DataCategory.TraceMetric, 3) + + val clientReport = clientReportRecorder.resetCountsAndGenerateClientReport() + val metricItem = + clientReport!!.discardedEvents!!.first { it.category == DataCategory.TraceMetric.category } + assertEquals(3, metricItem.quantity) + } + private fun givenClientReportRecorder( callback: Sentry.OptionsConfiguration? = null ) { From cceb10f689145521d9f1c14498ca7ad45535addc Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 19 Dec 2025 13:59:27 +0100 Subject: [PATCH 14/40] Metrics external options --- sentry/api/sentry.api | 2 ++ .../main/java/io/sentry/ExternalOptions.java | 11 +++++++++++ .../main/java/io/sentry/SentryOptions.java | 4 ++++ .../java/io/sentry/ExternalOptionsTest.kt | 19 +++++++++++++++++++ .../test/java/io/sentry/SentryOptionsTest.kt | 15 +++++++++++++++ 5 files changed, 51 insertions(+) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 9b9881d9f9..23561b1dba 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -513,6 +513,7 @@ public final class io/sentry/ExternalOptions { public fun isCaptureOpenTelemetryEvents ()Ljava/lang/Boolean; public fun isEnableBackpressureHandling ()Ljava/lang/Boolean; public fun isEnableLogs ()Ljava/lang/Boolean; + public fun isEnableMetrics ()Ljava/lang/Boolean; public fun isEnablePrettySerializationOutput ()Ljava/lang/Boolean; public fun isEnableSpotlight ()Ljava/lang/Boolean; public fun isEnabled ()Ljava/lang/Boolean; @@ -528,6 +529,7 @@ public final class io/sentry/ExternalOptions { public fun setEnableBackpressureHandling (Ljava/lang/Boolean;)V public fun setEnableDeduplication (Ljava/lang/Boolean;)V public fun setEnableLogs (Ljava/lang/Boolean;)V + public fun setEnableMetrics (Ljava/lang/Boolean;)V public fun setEnablePrettySerializationOutput (Ljava/lang/Boolean;)V public fun setEnableSpotlight (Ljava/lang/Boolean;)V public fun setEnableUncaughtExceptionHandler (Ljava/lang/Boolean;)V diff --git a/sentry/src/main/java/io/sentry/ExternalOptions.java b/sentry/src/main/java/io/sentry/ExternalOptions.java index f35e01af79..269e3729db 100644 --- a/sentry/src/main/java/io/sentry/ExternalOptions.java +++ b/sentry/src/main/java/io/sentry/ExternalOptions.java @@ -44,6 +44,7 @@ public final class ExternalOptions { private @Nullable Boolean enablePrettySerializationOutput; private @Nullable Boolean enableSpotlight; private @Nullable Boolean enableLogs; + private @Nullable Boolean enableMetrics; private @Nullable String spotlightConnectionUrl; private @Nullable List ignoredCheckIns; @@ -157,6 +158,8 @@ public final class ExternalOptions { options.setEnableLogs(propertiesProvider.getBooleanProperty("logs.enabled")); + options.setEnableMetrics(propertiesProvider.getBooleanProperty("metrics.enabled")); + for (final String ignoredExceptionType : propertiesProvider.getList("ignored-exceptions-for-type")) { try { @@ -539,6 +542,14 @@ public void setEnableLogs(final @Nullable Boolean enableLogs) { return enableLogs; } + public void setEnableMetrics(final @Nullable Boolean enableMetrics) { + this.enableMetrics = enableMetrics; + } + + public @Nullable Boolean isEnableMetrics() { + return enableMetrics; + } + public @Nullable Double getProfileSessionSampleRate() { return profileSessionSampleRate; } diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 724ee727ef..7f13163ce0 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -3480,6 +3480,10 @@ public void merge(final @NotNull ExternalOptions options) { getLogs().setEnabled(options.isEnableLogs()); } + if (options.isEnableMetrics() != null) { + getMetrics().setEnabled(options.isEnableMetrics()); + } + if (options.getProfileSessionSampleRate() != null) { setProfileSessionSampleRate(options.getProfileSessionSampleRate()); } diff --git a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt index 9ed3913f71..78eef96dfc 100644 --- a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt @@ -376,6 +376,25 @@ class ExternalOptionsTest { withPropertiesFile("logs.enabled=true") { options -> assertTrue(options.isEnableLogs == true) } } + @Test + fun `creates options with enableMetrics set to true`() { + withPropertiesFile("metrics.enabled=true") { options -> + assertTrue(options.isEnableMetrics == true) + } + } + + @Test + fun `creates options with enableMetrics set to false`() { + withPropertiesFile("metrics.enabled=false") { options -> + assertTrue(options.isEnableMetrics == false) + } + } + + @Test + fun `creates options with enableMetrics set to null when not set`() { + withPropertiesFile { assertNull(it.isEnableMetrics) } + } + @Test fun `creates options with profileSessionSampleRate set to 0_8`() { withPropertiesFile("profile-session-sample-rate=0.8") { options -> diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index e882f6fdc6..b2f7491b02 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -411,6 +411,7 @@ class SentryOptionsTest { externalOptions.spotlightConnectionUrl = "http://local.sentry.io:1234" externalOptions.isGlobalHubMode = true externalOptions.isEnableLogs = true + externalOptions.isEnableMetrics = false externalOptions.profileSessionSampleRate = 0.8 externalOptions.profilingTracesDirPath = "/profiling-traces" externalOptions.profileLifecycle = ProfileLifecycle.TRACE @@ -469,6 +470,7 @@ class SentryOptionsTest { assertEquals("http://local.sentry.io:1234", options.spotlightConnectionUrl) assertTrue(options.isGlobalHubMode!!) assertTrue(options.logs.isEnabled!!) + assertFalse(options.metrics.isEnabled) assertEquals(0.8, options.profileSessionSampleRate) assertEquals("/profiling-traces${File.separator}${hash}", options.profilingTracesDirPath) assertEquals(ProfileLifecycle.TRACE, options.profileLifecycle) @@ -482,6 +484,14 @@ class SentryOptionsTest { assertTrue(options.isEnableUncaughtExceptionHandler) } + @Test + fun `merging options when enableMetrics is not set preserves the default value`() { + val externalOptions = ExternalOptions() + val options = SentryOptions() + options.merge(externalOptions) + assertTrue(options.metrics.isEnabled) + } + @Test fun `merging options merges and overwrites existing tag values`() { val externalOptions = ExternalOptions() @@ -660,6 +670,11 @@ class SentryOptionsTest { assertTrue(SentryOptions().isEnableBackpressureHandling) } + @Test + fun `when options are initialized, metrics is enabled by default`() { + assertTrue(SentryOptions().metrics.isEnabled) + } + @Test fun `when options are initialized, enableSpotlight is set to false by default`() { assertFalse(SentryOptions().isEnableSpotlight) From 870ba66787de87cc326f3e0dbcd544ba667e42c2 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 19 Dec 2025 14:10:09 +0100 Subject: [PATCH 15/40] Metrics Manifest options for Android --- .../android/core/ManifestMetadataReader.java | 7 ++++ .../core/ManifestMetadataReaderTest.kt | 38 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java index 3acd0a779e..8cb301778b 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java @@ -144,6 +144,8 @@ final class ManifestMetadataReader { static final String ENABLE_LOGS = "io.sentry.logs.enabled"; + static final String ENABLE_METRICS = "io.sentry.metrics.enabled"; + static final String ENABLE_AUTO_TRACE_ID_GENERATION = "io.sentry.traces.enable-auto-id-generation"; @@ -614,6 +616,11 @@ static void applyMetadata( .getLogs() .setEnabled(readBool(metadata, logger, ENABLE_LOGS, options.getLogs().isEnabled())); + options + .getMetrics() + .setEnabled( + readBool(metadata, logger, ENABLE_METRICS, options.getMetrics().isEnabled())); + final @NotNull SentryFeedbackOptions feedbackOptions = options.getFeedbackOptions(); feedbackOptions.setNameRequired( readBool(metadata, logger, FEEDBACK_NAME_REQUIRED, feedbackOptions.isNameRequired())); diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt index 06d284c5dd..c17334120b 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt @@ -1711,6 +1711,44 @@ class ManifestMetadataReaderTest { assertTrue(fixture.options.logs.isEnabled) } + @Test + fun `applyMetadata reads metrics enabled and keep default value if not found`() { + // Arrange + val context = fixture.getContext() + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertTrue(fixture.options.metrics.isEnabled) + } + + @Test + fun `applyMetadata reads metrics enabled to options`() { + // Arrange + val bundle = bundleOf(ManifestMetadataReader.ENABLE_METRICS to false) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertFalse(fixture.options.metrics.isEnabled) + } + + @Test + fun `applyMetadata reads metrics enabled to options when set to true`() { + // Arrange + val bundle = bundleOf(ManifestMetadataReader.ENABLE_METRICS to true) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertTrue(fixture.options.metrics.isEnabled) + } + @Test fun `applyMetadata reads feedback name required and keep default value if not found`() { // Arrange From 56879b34dd776c29450bf6ea25baf13f764d0458 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 19 Dec 2025 14:44:28 +0100 Subject: [PATCH 16/40] Android Metrics batch processor and factory --- .../api/sentry-android-core.api | 12 +++ .../core/AndroidMetricsBatchProcessor.java | 49 ++++++++++ .../AndroidMetricsBatchProcessorFactory.java | 15 +++ .../core/AndroidOptionsInitializer.java | 1 + ...AndroidMetricsBatchProcessorFactoryTest.kt | 23 +++++ .../core/AndroidMetricsBatchProcessorTest.kt | 96 +++++++++++++++++++ 6 files changed, 196 insertions(+) create mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessor.java create mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessorFactory.java create mode 100644 sentry-android-core/src/test/java/io/sentry/android/core/AndroidMetricsBatchProcessorFactoryTest.kt create mode 100644 sentry-android-core/src/test/java/io/sentry/android/core/AndroidMetricsBatchProcessorTest.kt diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 0ff8f048a4..ff9a0c7597 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -100,6 +100,18 @@ public class io/sentry/android/core/AndroidMemoryCollector : io/sentry/IPerforma public fun setup ()V } +public final class io/sentry/android/core/AndroidMetricsBatchProcessor : io/sentry/metrics/MetricsBatchProcessor, io/sentry/android/core/AppState$AppStateListener { + public fun (Lio/sentry/SentryOptions;Lio/sentry/ISentryClient;)V + public fun close (Z)V + public fun onBackground ()V + public fun onForeground ()V +} + +public final class io/sentry/android/core/AndroidMetricsBatchProcessorFactory : io/sentry/metrics/IMetricsBatchProcessorFactory { + public fun ()V + public fun create (Lio/sentry/SentryOptions;Lio/sentry/SentryClient;)Lio/sentry/metrics/IMetricsBatchProcessor; +} + public class io/sentry/android/core/AndroidProfiler { protected final field lock Lio/sentry/util/AutoClosableReentrantLock; public fun (Ljava/lang/String;ILio/sentry/android/core/internal/util/SentryFrameMetricsCollector;Lio/sentry/ISentryExecutorService;Lio/sentry/ILogger;)V diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessor.java new file mode 100644 index 0000000000..64c612a886 --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessor.java @@ -0,0 +1,49 @@ +package io.sentry.android.core; + +import io.sentry.ISentryClient; +import io.sentry.SentryLevel; +import io.sentry.SentryOptions; +import io.sentry.metrics.MetricsBatchProcessor; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public final class AndroidMetricsBatchProcessor extends MetricsBatchProcessor + implements AppState.AppStateListener { + + public AndroidMetricsBatchProcessor( + @NotNull SentryOptions options, @NotNull ISentryClient client) { + super(options, client); + AppState.getInstance().addAppStateListener(this); + } + + @Override + public void onForeground() { + // no-op + } + + @Override + public void onBackground() { + try { + options + .getExecutorService() + .submit( + new Runnable() { + @Override + public void run() { + flush(MetricsBatchProcessor.FLUSH_AFTER_MS); + } + }); + } catch (Throwable t) { + options + .getLogger() + .log(SentryLevel.ERROR, t, "Failed to submit metrics flush in onBackground()"); + } + } + + @Override + public void close(boolean isRestarting) { + AppState.getInstance().removeAppStateListener(this); + super.close(isRestarting); + } +} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessorFactory.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessorFactory.java new file mode 100644 index 0000000000..eef0a6311f --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessorFactory.java @@ -0,0 +1,15 @@ +package io.sentry.android.core; + +import io.sentry.SentryClient; +import io.sentry.SentryOptions; +import io.sentry.metrics.IMetricsBatchProcessor; +import io.sentry.metrics.IMetricsBatchProcessorFactory; +import org.jetbrains.annotations.NotNull; + +public final class AndroidMetricsBatchProcessorFactory implements IMetricsBatchProcessorFactory { + @Override + public @NotNull IMetricsBatchProcessor create( + @NotNull SentryOptions options, @NotNull SentryClient client) { + return new AndroidMetricsBatchProcessor(options, client); + } +} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index 1243c3acb5..b7bb5bf21a 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -125,6 +125,7 @@ static void loadDefaultAndMetadataOptions( options.setDateProvider(new SentryAndroidDateProvider()); options.setRuntimeManager(new AndroidRuntimeManager()); options.getLogs().setLoggerBatchProcessorFactory(new AndroidLoggerBatchProcessorFactory()); + options.getMetrics().setMetricsBatchProcessorFactory(new AndroidMetricsBatchProcessorFactory()); // set a lower flush timeout on Android to avoid ANRs options.setFlushTimeoutMillis(DEFAULT_FLUSH_TIMEOUT_MS); diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidMetricsBatchProcessorFactoryTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidMetricsBatchProcessorFactoryTest.kt new file mode 100644 index 0000000000..9ddaa36abf --- /dev/null +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidMetricsBatchProcessorFactoryTest.kt @@ -0,0 +1,23 @@ +package io.sentry.android.core + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.sentry.SentryClient +import kotlin.test.Test +import kotlin.test.assertIs +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +@RunWith(AndroidJUnit4::class) +class AndroidMetricsBatchProcessorFactoryTest { + + @Test + fun `create returns AndroidMetricsBatchProcessor instance`() { + val factory = AndroidMetricsBatchProcessorFactory() + val options = SentryAndroidOptions() + val client: SentryClient = mock() + + val processor = factory.create(options, client) + + assertIs(processor) + } +} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidMetricsBatchProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidMetricsBatchProcessorTest.kt new file mode 100644 index 0000000000..fceb9ed3f4 --- /dev/null +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidMetricsBatchProcessorTest.kt @@ -0,0 +1,96 @@ +package io.sentry.android.core + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.sentry.ISentryClient +import io.sentry.SentryMetricsEvent +import io.sentry.SentryOptions +import io.sentry.protocol.SentryId +import io.sentry.test.ImmediateExecutorService +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@RunWith(AndroidJUnit4::class) +class AndroidMetricsBatchProcessorTest { + + private class Fixture { + val options = SentryAndroidOptions() + val client: ISentryClient = mock() + + fun getSut( + useImmediateExecutor: Boolean = false, + config: ((SentryOptions) -> Unit)? = null, + ): AndroidMetricsBatchProcessor { + if (useImmediateExecutor) { + options.executorService = ImmediateExecutorService() + } + config?.invoke(options) + return AndroidMetricsBatchProcessor(options, client) + } + } + + private val fixture = Fixture() + + @BeforeTest + fun `set up`() { + AppState.getInstance().resetInstance() + } + + @AfterTest + fun `tear down`() { + AppState.getInstance().resetInstance() + } + + @Test + fun `constructor registers as AppState listener`() { + fixture.getSut() + assertNotNull(AppState.getInstance().lifecycleObserver) + } + + @Test + fun `onBackground schedules flush`() { + val sut = fixture.getSut(useImmediateExecutor = true) + val metricsEvent = SentryMetricsEvent(SentryId(), 1.0, "test", "counter", 3.0) + sut.add(metricsEvent) + + sut.onBackground() + + verify(fixture.client).captureBatchedMetricsEvents(any()) + } + + @Test + fun `onBackground handles executor exception gracefully`() { + val sut = + fixture.getSut { options -> + val rejectingExecutor = mock() + whenever(rejectingExecutor.submit(any())).thenThrow(RuntimeException("Rejected")) + options.executorService = rejectingExecutor + } + + // Should not throw + sut.onBackground() + } + + @Test + fun `close removes AppState listener`() { + val sut = fixture.getSut() + sut.close(false) + + assertTrue(AppState.getInstance().lifecycleObserver.listeners.isEmpty()) + } + + @Test + fun `close with isRestarting true still removes listener`() { + val sut = fixture.getSut() + sut.close(true) + + assertTrue(AppState.getInstance().lifecycleObserver.listeners.isEmpty()) + } +} From 15ec6da63c692593fc6cb0f20cf39532a0b1d88e Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 22 Dec 2025 13:44:35 +0100 Subject: [PATCH 17/40] Only send user attributes if sendDefaultPii is true --- sentry/src/main/java/io/sentry/metrics/MetricsApi.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java index 71ec6118ee..894bd7aba2 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java @@ -231,7 +231,9 @@ private void captureMetrics( setServerName(attributes); } - setUser(attributes); + if (scopes.getOptions().isSendDefaultPii()) { + setUser(attributes); + } return attributes; } From 52863e30d04e16a005dadf7293adead373c08adb Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 22 Dec 2025 14:39:43 +0100 Subject: [PATCH 18/40] Remove Experimental Annotation from Metrics Options --- sentry/src/main/java/io/sentry/SentryOptions.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 7f13163ce0..f14b376c42 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -3531,12 +3531,10 @@ public void setLogs(@NotNull SentryOptions.Logs logs) { this.logs = logs; } - @ApiStatus.Experimental public @NotNull SentryOptions.Metrics getMetrics() { return metrics; } - @ApiStatus.Experimental public void setMetrics(@NotNull SentryOptions.Metrics metrics) { this.metrics = metrics; } From a79f91b34961ef75c26dca1e00f9d747c7412bab Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 5 Jan 2026 13:43:30 +0100 Subject: [PATCH 19/40] Add unit tests for Metrics --- sentry/src/test/java/io/sentry/ScopesTest.kt | 852 ++++++++++++++++++ .../test/java/io/sentry/SentryClientTest.kt | 69 ++ .../metrics/MetricsBatchProcessorTest.kt | 90 ++ .../SentryMetricsSerializationTest.kt | 96 ++ .../test/resources/json/sentry_metrics.json | 59 ++ 5 files changed, 1166 insertions(+) create mode 100644 sentry/src/test/java/io/sentry/metrics/MetricsBatchProcessorTest.kt create mode 100644 sentry/src/test/java/io/sentry/protocol/SentryMetricsSerializationTest.kt create mode 100644 sentry/src/test/resources/json/sentry_metrics.json diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index 4eee9d1636..dde633d878 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -3125,6 +3125,858 @@ class ScopesTest { // endregion + // region metrics + + @Test + fun `when captureMetric is called on disabled client, do nothing`() { + val (sut, mockClient) = getEnabledScopes() + sut.close() + + sut.metrics().count("metric name") + verify(mockClient, never()).captureMetric(any(), anyOrNull()) + } + + @Test + fun `when metrics is not enabled, do nothing`() { + val (sut, mockClient) = getEnabledScopes { it.metrics.isEnabled = false } + + sut.metrics().count("metric name") + verify(mockClient, never()).captureMetric(any(), anyOrNull()) + } + + @Test + fun `creating count metric works`() { + val (sut, mockClient) = getEnabledScopes() + + sut.metrics().count("count metric") + + verify(mockClient) + .captureMetric( + check { + assertEquals("count metric", it.name) + assertEquals(1.0, it.value) + assertEquals("counter", it.type) + }, + anyOrNull(), + ) + } + + @Test + fun `creating gauge metric works`() { + val (sut, mockClient) = getEnabledScopes() + + sut.metrics().gauge("gauge metric", 2.3) + + verify(mockClient) + .captureMetric( + check { + assertEquals("gauge metric", it.name) + assertEquals(2.3, it.value) + assertEquals("gauge", it.type) + }, + anyOrNull(), + ) + } + + @Test + fun `creating distribution metric works`() { + val (sut, mockClient) = getEnabledScopes() + + sut.metrics().distribution("distribution metric", 3.4) + + verify(mockClient) + .captureMetric( + check { + assertEquals("distribution metric", it.name) + assertEquals(3.4, it.value) + assertEquals("distribution", it.type) + }, + anyOrNull(), + ) + } + + @Test + fun `metric with manual origin does not have origin attribute`() { + val (sut, mockClient) = getEnabledScopes() + + sut.metrics().count("metric name") + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + assertNull(it.attributes!!.get("sentry.origin")) + }, + anyOrNull(), + ) + } + + @Test + fun `metric with non manual origin does have origin attribute`() { + val (sut, mockClient) = getEnabledScopes { it.logs.isEnabled = true } + + sut + .metrics() + .count("metric name", 1.0, "visit", SentryLogParameters().also { it.origin = "other" }) + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + assertEquals( + "other", + (it.attributes!!.get("sentry.origin") as? SentryLogEventAttributeValue)?.value, + ) + }, + anyOrNull(), + ) + } + + @Test + fun `creating count metric with value and unit works`() { + val (sut, mockClient) = getEnabledScopes() + + sut.metrics().count("metric name", 1.0, "visit") + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + assertEquals(1.0, it.value) + assertEquals("visit", it.unit) + assertEquals("counter", it.type) + }, + anyOrNull(), + ) + } + + @Test + fun `creating count metric with value works`() { + val (sut, mockClient) = getEnabledScopes() + + sut.metrics().count("metric name", 1.0) + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + assertEquals(1.0, it.value) + assertEquals("counter", it.type) + }, + anyOrNull(), + ) + } + + @Test + fun `creating count metric with unit works`() { + val (sut, mockClient) = getEnabledScopes() + + sut.metrics().count("metric name", "visit") + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + assertEquals(1.0, it.value) + assertEquals("visit", it.unit) + assertEquals("counter", it.type) + }, + anyOrNull(), + ) + } + + @Test + fun `creating count metric with attributes from map works`() { + val (sut, mockClient) = getEnabledScopes() + + sut + .metrics() + .count( + "metric name", + 1.0, + "visit", + SentryLogParameters.create(SentryAttributes.fromMap(mapOf("attrname1" to "attrval1"))), + ) + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + assertEquals(1.0, it.value) + assertEquals("visit", it.unit) + assertEquals("counter", it.type) + + val attr1 = it.attributes?.get("attrname1")!! + assertEquals("attrval1", attr1.value) + assertEquals("string", attr1.type) + }, + anyOrNull(), + ) + } + + @Test + fun `creating count metric with attributes works`() { + val (sut, mockClient) = getEnabledScopes() + + sut + .metrics() + .count( + "metric name", + 1.0, + "visit", + SentryLogParameters.create( + SentryAttributes.of( + SentryAttribute.stringAttribute("strattr", "strval"), + SentryAttribute.booleanAttribute("boolattr", true), + SentryAttribute.integerAttribute("intattr", 17), + SentryAttribute.doubleAttribute("doubleattr", 3.8), + SentryAttribute.named("namedstrattr", "namedstrval"), + SentryAttribute.named("namedboolattr", false), + SentryAttribute.named("namedintattr", 18), + SentryAttribute.named("nameddoubleattr", 4.9), + ) + ), + ) + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + assertEquals(1.0, it.value) + assertEquals("visit", it.unit) + assertEquals("counter", it.type) + + val strattr = it.attributes?.get("strattr")!! + assertEquals("strval", strattr.value) + assertEquals("string", strattr.type) + + val boolattr = it.attributes?.get("boolattr")!! + assertEquals(true, boolattr.value) + assertEquals("boolean", boolattr.type) + + val intattr = it.attributes?.get("intattr")!! + assertEquals(17, intattr.value) + assertEquals("integer", intattr.type) + + val doubleattr = it.attributes?.get("doubleattr")!! + assertEquals(3.8, doubleattr.value) + assertEquals("double", doubleattr.type) + + val namedstrattr = it.attributes?.get("namedstrattr")!! + assertEquals("namedstrval", namedstrattr.value) + assertEquals("string", namedstrattr.type) + + val namedboolattr = it.attributes?.get("namedboolattr")!! + assertEquals(false, namedboolattr.value) + assertEquals("boolean", namedboolattr.type) + + val namedintattr = it.attributes?.get("namedintattr")!! + assertEquals(18, namedintattr.value) + assertEquals("integer", namedintattr.type) + + val nameddoubleattr = it.attributes?.get("nameddoubleattr")!! + assertEquals(4.9, nameddoubleattr.value) + assertEquals("double", nameddoubleattr.type) + }, + anyOrNull(), + ) + } + + @Test + fun `creating count metric with attributes and timestamp works`() { + val (sut, mockClient) = getEnabledScopes() + + sut + .metrics() + .count( + "metric name", + 1.0, + "visit", + SentryLogParameters.create( + SentryLongDate(123), + SentryAttributes.of(SentryAttribute.named("attrname1", "attrval1")), + ), + ) + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + assertEquals(1.0, it.value) + assertEquals("visit", it.unit) + assertEquals("counter", it.type) + + val attr1 = it.attributes?.get("attrname1")!! + assertEquals("attrval1", attr1.value) + assertEquals("string", attr1.type) + }, + anyOrNull(), + ) + } + + @Test + fun `creating distribution metric with value and unit works`() { + val (sut, mockClient) = getEnabledScopes() + + sut.metrics().distribution("metric name", 1.0, "ms") + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + assertEquals(1.0, it.value) + assertEquals("ms", it.unit) + assertEquals("distribution", it.type) + }, + anyOrNull(), + ) + } + + @Test + fun `creating distribution metric with value works`() { + val (sut, mockClient) = getEnabledScopes() + + sut.metrics().distribution("metric name", 1.0) + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + assertEquals(1.0, it.value) + assertEquals("distribution", it.type) + }, + anyOrNull(), + ) + } + + @Test + fun `creating distribution metric with attributes from map works`() { + val (sut, mockClient) = getEnabledScopes() + + sut + .metrics() + .distribution( + "metric name", + 3.7, + "ms", + SentryLogParameters.create(SentryAttributes.fromMap(mapOf("attrname1" to "attrval1"))), + ) + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + assertEquals(3.7, it.value) + assertEquals("ms", it.unit) + assertEquals("distribution", it.type) + + val attr1 = it.attributes?.get("attrname1")!! + assertEquals("attrval1", attr1.value) + assertEquals("string", attr1.type) + }, + anyOrNull(), + ) + } + + @Test + fun `creating distribution metric with attributes works`() { + val (sut, mockClient) = getEnabledScopes() + + sut + .metrics() + .distribution( + "metric name", + 3.7, + "ms", + SentryLogParameters.create( + SentryAttributes.of( + SentryAttribute.stringAttribute("strattr", "strval"), + SentryAttribute.booleanAttribute("boolattr", true), + SentryAttribute.integerAttribute("intattr", 17), + SentryAttribute.doubleAttribute("doubleattr", 3.8), + SentryAttribute.named("namedstrattr", "namedstrval"), + SentryAttribute.named("namedboolattr", false), + SentryAttribute.named("namedintattr", 18), + SentryAttribute.named("nameddoubleattr", 4.9), + ) + ), + ) + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + assertEquals(3.7, it.value) + assertEquals("ms", it.unit) + assertEquals("distribution", it.type) + + val strattr = it.attributes?.get("strattr")!! + assertEquals("strval", strattr.value) + assertEquals("string", strattr.type) + + val boolattr = it.attributes?.get("boolattr")!! + assertEquals(true, boolattr.value) + assertEquals("boolean", boolattr.type) + + val intattr = it.attributes?.get("intattr")!! + assertEquals(17, intattr.value) + assertEquals("integer", intattr.type) + + val doubleattr = it.attributes?.get("doubleattr")!! + assertEquals(3.8, doubleattr.value) + assertEquals("double", doubleattr.type) + + val namedstrattr = it.attributes?.get("namedstrattr")!! + assertEquals("namedstrval", namedstrattr.value) + assertEquals("string", namedstrattr.type) + + val namedboolattr = it.attributes?.get("namedboolattr")!! + assertEquals(false, namedboolattr.value) + assertEquals("boolean", namedboolattr.type) + + val namedintattr = it.attributes?.get("namedintattr")!! + assertEquals(18, namedintattr.value) + assertEquals("integer", namedintattr.type) + + val nameddoubleattr = it.attributes?.get("nameddoubleattr")!! + assertEquals(4.9, nameddoubleattr.value) + assertEquals("double", nameddoubleattr.type) + }, + anyOrNull(), + ) + } + + @Test + fun `creating distribution metric with attributes and timestamp works`() { + val (sut, mockClient) = getEnabledScopes() + + sut + .metrics() + .distribution( + "metric name", + 3.7, + "ms", + SentryLogParameters.create( + SentryLongDate(123), + SentryAttributes.of(SentryAttribute.named("attrname1", "attrval1")), + ), + ) + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + assertEquals(3.7, it.value) + assertEquals("ms", it.unit) + assertEquals("distribution", it.type) + + val attr1 = it.attributes?.get("attrname1")!! + assertEquals("attrval1", attr1.value) + assertEquals("string", attr1.type) + }, + anyOrNull(), + ) + } + + @Test + fun `creating gauge metric with value and unit works`() { + val (sut, mockClient) = getEnabledScopes() + + sut.metrics().gauge("metric name", 128.0, "byte") + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + assertEquals(128.0, it.value) + assertEquals("byte", it.unit) + assertEquals("gauge", it.type) + }, + anyOrNull(), + ) + } + + @Test + fun `creating gauge metric with value works`() { + val (sut, mockClient) = getEnabledScopes() + + sut.metrics().gauge("metric name", 128.0) + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + assertEquals(128.0, it.value) + assertEquals("gauge", it.type) + }, + anyOrNull(), + ) + } + + @Test + fun `creating gauge metric with attributes from map works`() { + val (sut, mockClient) = getEnabledScopes() + + sut + .metrics() + .gauge( + "metric name", + 256.0, + "byte", + SentryLogParameters.create(SentryAttributes.fromMap(mapOf("attrname1" to "attrval1"))), + ) + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + assertEquals(256.0, it.value) + assertEquals("byte", it.unit) + assertEquals("gauge", it.type) + + val attr1 = it.attributes?.get("attrname1")!! + assertEquals("attrval1", attr1.value) + assertEquals("string", attr1.type) + }, + anyOrNull(), + ) + } + + @Test + fun `creating gauge metric with attributes works`() { + val (sut, mockClient) = getEnabledScopes() + + sut + .metrics() + .gauge( + "metric name", + 256.0, + "byte", + SentryLogParameters.create( + SentryAttributes.of( + SentryAttribute.stringAttribute("strattr", "strval"), + SentryAttribute.booleanAttribute("boolattr", true), + SentryAttribute.integerAttribute("intattr", 17), + SentryAttribute.doubleAttribute("doubleattr", 3.8), + SentryAttribute.named("namedstrattr", "namedstrval"), + SentryAttribute.named("namedboolattr", false), + SentryAttribute.named("namedintattr", 18), + SentryAttribute.named("nameddoubleattr", 4.9), + ) + ), + ) + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + assertEquals(256.0, it.value) + assertEquals("byte", it.unit) + assertEquals("gauge", it.type) + + val strattr = it.attributes?.get("strattr")!! + assertEquals("strval", strattr.value) + assertEquals("string", strattr.type) + + val boolattr = it.attributes?.get("boolattr")!! + assertEquals(true, boolattr.value) + assertEquals("boolean", boolattr.type) + + val intattr = it.attributes?.get("intattr")!! + assertEquals(17, intattr.value) + assertEquals("integer", intattr.type) + + val doubleattr = it.attributes?.get("doubleattr")!! + assertEquals(3.8, doubleattr.value) + assertEquals("double", doubleattr.type) + + val namedstrattr = it.attributes?.get("namedstrattr")!! + assertEquals("namedstrval", namedstrattr.value) + assertEquals("string", namedstrattr.type) + + val namedboolattr = it.attributes?.get("namedboolattr")!! + assertEquals(false, namedboolattr.value) + assertEquals("boolean", namedboolattr.type) + + val namedintattr = it.attributes?.get("namedintattr")!! + assertEquals(18, namedintattr.value) + assertEquals("integer", namedintattr.type) + + val nameddoubleattr = it.attributes?.get("nameddoubleattr")!! + assertEquals(4.9, nameddoubleattr.value) + assertEquals("double", nameddoubleattr.type) + }, + anyOrNull(), + ) + } + + @Test + fun `creating gauge metric with attributes and timestamp works`() { + val (sut, mockClient) = getEnabledScopes() + + sut + .metrics() + .gauge( + "metric name", + 256.0, + "byte", + SentryLogParameters.create( + SentryLongDate(123), + SentryAttributes.of(SentryAttribute.named("attrname1", "attrval1")), + ), + ) + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + assertEquals(256.0, it.value) + assertEquals("byte", it.unit) + assertEquals("gauge", it.type) + + val attr1 = it.attributes?.get("attrname1")!! + assertEquals("attrval1", attr1.value) + assertEquals("string", attr1.type) + }, + anyOrNull(), + ) + } + + @Test + fun `adds user fields to metric attributes if sendDefaultPii is true`() { + val (sut, mockClient) = + getEnabledScopes { + it.distinctId = "distinctId" + it.isSendDefaultPii = true + } + + sut.configureScope { scope -> + scope.user = + User().also { + it.id = "usrid" + it.username = "usrname" + it.email = "user@sentry.io" + } + } + sut.metrics().count("metric name") + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + + val userId = it.attributes?.get("user.id")!! + assertEquals("usrid", userId.value) + assertEquals("string", userId.type) + + val userName = it.attributes?.get("user.name")!! + assertEquals("usrname", userName.value) + assertEquals("string", userName.type) + + val userEmail = it.attributes?.get("user.email")!! + assertEquals("user@sentry.io", userEmail.value) + assertEquals("string", userEmail.type) + }, + anyOrNull(), + ) + } + + @Test + fun `does not add user fields to metric attributes by default`() { + val (sut, mockClient) = getEnabledScopes { it.distinctId = "distinctId" } + + sut.configureScope { scope -> + scope.user = + User().also { + it.id = "usrid" + it.username = "usrname" + it.email = "user@sentry.io" + } + } + sut.metrics().count("metric name") + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + + assertNull(it.attributes?.get("user.id")) + assertNull(it.attributes?.get("user.name")) + assertNull(it.attributes?.get("user.email")) + }, + anyOrNull(), + ) + } + + @Test + fun `unset user does provide distinct-id as user-id for metrics`() { + val (sut, mockClient) = + getEnabledScopes { + it.isSendDefaultPii = true + it.distinctId = "distinctId" + } + + sut.metrics().count("metric name") + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + + assertEquals("distinctId", it.attributes?.get("user.id")?.value) + assertNull(it.attributes?.get("user.name")) + assertNull(it.attributes?.get("user.email")) + }, + anyOrNull(), + ) + } + + @Test + fun `unset user does provide null user-id when distinct-id is missing for metrics`() { + val (sut, mockClient) = + getEnabledScopes { + it.isSendDefaultPii = true + it.distinctId = null + } + + sut.metrics().count("metric name") + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + assertNull(it.attributes?.get("user.id")) + assertNull(it.attributes?.get("user.name")) + assertNull(it.attributes?.get("user.email")) + }, + anyOrNull(), + ) + } + + @Test + fun `missing user fields do not break attributes for metrics`() { + val (sut, mockClient) = + getEnabledScopes { + it.isSendDefaultPii = true + it.distinctId = "distinctId" + } + + sut.configureScope { scope -> scope.user = User() } + sut.metrics().count("metric name") + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + + assertNull(it.attributes?.get("user.id")) + assertNull(it.attributes?.get("user.name")) + assertNull(it.attributes?.get("user.email")) + }, + anyOrNull(), + ) + } + + @Test + fun `adds session replay id to metric attributes`() { + val (sut, mockClient) = getEnabledScopes() + val replayId = SentryId() + sut.scope.replayId = replayId + sut.metrics().count("metric name") + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + val logReplayId = it.attributes?.get("sentry.replay_id")!! + assertEquals(replayId.toString(), logReplayId.value) + }, + anyOrNull(), + ) + } + + @Test + fun `missing session replay id do not break metric attributes`() { + val (sut, mockClient) = getEnabledScopes() + sut.metrics().count("metric name") + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + val logReplayId = it.attributes?.get("sentry.replay_id") + assertNull(logReplayId) + }, + anyOrNull(), + ) + } + + @Test + fun `does not add session replay buffering to metric attributes if no replay id in scope and in controller`() { + val (sut, mockClient) = getEnabledScopes() + + sut.metrics().count("metric name") + assertEquals(SentryId.EMPTY_ID, sut.options.replayController.replayId) + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + val logReplayId = it.attributes?.get("sentry.replay_id") + val logReplayType = it.attributes?.get("sentry._internal.replay_is_buffering") + assertNull(logReplayId) + assertNull(logReplayType) + }, + anyOrNull(), + ) + } + + @Test + fun `does not add session replay buffering to metric attributes if replay id in scope`() { + val (sut, mockClient) = getEnabledScopes() + val replayId = SentryId() + sut.scope.replayId = replayId + + sut.metrics().count("metric name") + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + val logReplayId = it.attributes?.get("sentry.replay_id") + val logReplayType = it.attributes?.get("sentry._internal.replay_is_buffering") + assertEquals(replayId.toString(), logReplayId!!.value) + assertNull(logReplayType) + }, + anyOrNull(), + ) + } + + @Test + fun `adds session replay buffering to metric attributes if replay id in controller and not in scope`() { + val mockReplayController = mock() + val (sut, mockClient) = getEnabledScopes { it.setReplayController(mockReplayController) } + val replayId = SentryId() + sut.scope.replayId = SentryId.EMPTY_ID + whenever(mockReplayController.replayId).thenReturn(replayId) + + sut.metrics().count("metric name") + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + val logReplayId = it.attributes?.get("sentry.replay_id") + val logReplayType = it.attributes?.get("sentry._internal.replay_is_buffering")!! + assertEquals(replayId.toString(), logReplayId!!.value) + assertTrue(logReplayType.value as Boolean) + }, + anyOrNull(), + ) + } + + // endregion + @Test fun `null tags do not cause NPE`() { val scopes = generateScopes() diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 6d6165e0e8..28d176d85b 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -14,6 +14,7 @@ import io.sentry.hints.Cached import io.sentry.hints.DiskFlushNotification import io.sentry.hints.TransactionEnd import io.sentry.logger.ILoggerBatchProcessor +import io.sentry.metrics.IMetricsBatchProcessor import io.sentry.protocol.Contexts import io.sentry.protocol.Feedback import io.sentry.protocol.Mechanism @@ -338,6 +339,74 @@ class SentryClientTest { verifyNoMoreInteractions(batchProcessor) } + @Test + fun `when beforeSendMetric is set, callback is invoked`() { + val scope = createScope() + var invoked = false + fixture.sentryOptions.metrics.setBeforeSend { m -> + invoked = true + m + } + val sut = fixture.getSut() + sut.captureMetric( + SentryMetricsEvent(SentryId(), SentryNanotimeDate(), "name", "gauge", 123.0), + scope, + ) + assertTrue(invoked) + } + + @Test + fun `when beforeSendMetric returns null, metric is dropped`() { + val scope = createScope() + fixture.sentryOptions.metrics.setBeforeSend { _: SentryMetricsEvent -> null } + val sut = fixture.getSut() + sut.captureMetric( + SentryMetricsEvent(SentryId(), SentryNanotimeDate(), "name", "gauge", 123.0), + scope, + ) + verify(fixture.transport, never()).send(any(), anyOrNull()) + + assertClientReport( + fixture.sentryOptions.clientReportRecorder, + listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.TraceMetric.category, 1)), + ) + } + + @Test + fun `when beforeSendMetric throws an exception, metric is dropped`() { + val scope = createScope() + val exception = Exception("test") + + exception.stackTrace.toString() + fixture.sentryOptions.metrics.setBeforeSend { _ -> throw exception } + val sut = fixture.getSut() + sut.captureMetric( + SentryMetricsEvent(SentryId(), SentryNanotimeDate(), "name", "gauge", 123.0), + scope, + ) + + assertClientReport( + fixture.sentryOptions.clientReportRecorder, + listOf(DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.TraceMetric.category, 1)), + ) + } + + @Test + fun `when beforeSendMetric is returns new instance, new instance is sent`() { + val scope = createScope() + val expected = + SentryMetricsEvent(SentryId(), SentryNanotimeDate(), "expected name", "gauge", 123.0) + fixture.sentryOptions.metrics.setBeforeSend { _ -> expected } + val sut = fixture.getSut() + val batchProcessor = mock() + sut.injectForField("metricsBatchProcessor", batchProcessor) + val actual = + SentryMetricsEvent(SentryId(), SentryNanotimeDate(), "actual name", "counter", 97.0) + sut.captureMetric(actual, scope) + verify(batchProcessor).add(check { assertEquals("expected name", it.name) }) + verifyNoMoreInteractions(batchProcessor) + } + @Test fun `when event captured with hint, hint passed to connection`() { val event = SentryEvent() diff --git a/sentry/src/test/java/io/sentry/metrics/MetricsBatchProcessorTest.kt b/sentry/src/test/java/io/sentry/metrics/MetricsBatchProcessorTest.kt new file mode 100644 index 0000000000..da83ad882f --- /dev/null +++ b/sentry/src/test/java/io/sentry/metrics/MetricsBatchProcessorTest.kt @@ -0,0 +1,90 @@ +package io.sentry.metrics + +import io.sentry.DataCategory +import io.sentry.ISentryClient +import io.sentry.SentryMetricsEvent +import io.sentry.SentryMetricsEvents +import io.sentry.SentryNanotimeDate +import io.sentry.SentryOptions +import io.sentry.clientreport.ClientReportTestHelper +import io.sentry.clientreport.DiscardReason +import io.sentry.clientreport.DiscardedEvent +import io.sentry.protocol.SentryId +import io.sentry.test.DeferredExecutorService +import io.sentry.test.injectForField +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.atLeast +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +class MetricsBatchProcessorTest { + @Test + fun `drops metrics events after reaching MAX_QUEUE_SIZE limit`() { + // given + val mockClient = mock() + val mockExecutor = DeferredExecutorService() + val options = SentryOptions() + val processor = MetricsBatchProcessor(options, mockClient) + processor.injectForField("executorService", mockExecutor) + + for (i in 1..10001) { + val logEvent = + SentryMetricsEvent(SentryId(), SentryNanotimeDate(), "name $i", "gauge", i.toDouble()) + processor.add(logEvent) + } + + // run twice since a non full batch would be scheduled at the end + mockExecutor.runAll() + mockExecutor.runAll() + + // assert that the transport received 10000 metrics events + val captor = argumentCaptor() + verify(mockClient, atLeast(1)).captureBatchedMetricsEvents(captor.capture()) + + val allCapturedEvents = mutableListOf() + captor.allValues.forEach { metricsEvents -> allCapturedEvents.addAll(metricsEvents.items) } + + assertEquals(10000, allCapturedEvents.size) + + // assert that metric 10001 did not make it but metric 10000 did get sent + val metric10000Found = allCapturedEvents.any { it.name == "name 10000" } + val metric10001Found = allCapturedEvents.any { it.name == "name 10001" } + + assertTrue(metric10000Found, "Metric 10000 should have been sent") + assertFalse(metric10001Found, "Metric 10001 should not have been sent") + } + + @Test + fun `records client report when log event is dropped due to queue overflow`() { + // given + val mockClient = mock() + val mockExecutor = DeferredExecutorService() + val options = SentryOptions() + val processor = MetricsBatchProcessor(options, mockClient) + processor.injectForField("executorService", mockExecutor) + + // fill the queue to MAX_QUEUE_SIZE + for (i in 1..10000) { + val logEvent = + SentryMetricsEvent(SentryId(), SentryNanotimeDate(), "name $i", "gauge", i.toDouble()) + processor.add(logEvent) + } + + // add one more metrics event that should be dropped + val droppedMetricsEvent = + SentryMetricsEvent(SentryId(), SentryNanotimeDate(), "dropped metric", "gauge", 10001.0) + processor.add(droppedMetricsEvent) + + // verify that a client report was recorded for the dropped metrics item + val expectedEvents = + mutableListOf( + DiscardedEvent(DiscardReason.QUEUE_OVERFLOW.reason, DataCategory.TraceMetric.category, 1) + ) + + ClientReportTestHelper.assertClientReport(options.clientReportRecorder, expectedEvents) + } +} diff --git a/sentry/src/test/java/io/sentry/protocol/SentryMetricsSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/SentryMetricsSerializationTest.kt new file mode 100644 index 0000000000..2ce89e0af4 --- /dev/null +++ b/sentry/src/test/java/io/sentry/protocol/SentryMetricsSerializationTest.kt @@ -0,0 +1,96 @@ +package io.sentry.protocol + +import io.sentry.DateUtils +import io.sentry.FileFromResources +import io.sentry.ILogger +import io.sentry.JsonObjectReader +import io.sentry.JsonObjectWriter +import io.sentry.JsonSerializable +import io.sentry.SentryAttributeType +import io.sentry.SentryLogEventAttributeValue +import io.sentry.SentryMetricsEvent +import io.sentry.SentryMetricsEvents +import io.sentry.SpanId +import java.io.StringReader +import java.io.StringWriter +import kotlin.test.assertEquals +import org.junit.Test +import org.mockito.kotlin.mock + +class SentryMetricsSerializationTest { + class Fixture { + val logger = mock() + + fun getSut() = + SentryMetricsEvents( + listOf( + SentryMetricsEvent( + SentryId("5c1f73d39486827b9e60ceb1fc23277a"), + DateUtils.dateToSeconds(DateUtils.getDateTime("2004-04-10T18:24:03.000Z")), + "42e6bd2a-c45e-414d-8066-ed5196fbc686", + "counter", + 123.0, + ) + .also { + it.spanId = SpanId("f28b86350e534671") + it.unit = "visit" + it.attributes = + mutableMapOf( + "sentry.sdk.name" to + SentryLogEventAttributeValue("string", "sentry.java.spring-boot.jakarta"), + "sentry.environment" to SentryLogEventAttributeValue("string", "production"), + "sentry.sdk.version" to SentryLogEventAttributeValue("string", "8.11.1"), + "sentry.trace.parent_span_id" to + SentryLogEventAttributeValue("string", "f28b86350e534671"), + "custom.boolean" to SentryLogEventAttributeValue("boolean", true), + "custom.point2" to + SentryLogEventAttributeValue(SentryAttributeType.STRING, Point(21, 31)), + "custom.double" to SentryLogEventAttributeValue("double", 11.12.toDouble()), + "custom.point" to SentryLogEventAttributeValue("string", Point(20, 30)), + "custom.integer" to SentryLogEventAttributeValue("integer", 10), + ) + } + ) + ) + } + + private val fixture = Fixture() + + @Test + fun serialize() { + val expected = sanitizedFile("json/sentry_metrics.json") + val actual = serialize(fixture.getSut()) + assertEquals(expected, actual) + } + + @Test + fun deserialize() { + val expectedJson = sanitizedFile("json/sentry_metrics.json") + val actual = deserialize(expectedJson) + val actualJson = serialize(actual) + assertEquals(expectedJson, actualJson) + } + + // Helper + + private fun sanitizedFile(path: String): String = + FileFromResources.invoke(path).replace(Regex("[\n\r]"), "").replace(" ", "") + + private fun serialize(jsonSerializable: JsonSerializable): String { + val wrt = StringWriter() + val jsonWrt = JsonObjectWriter(wrt, 100) + jsonSerializable.serialize(jsonWrt, fixture.logger) + return wrt.toString() + } + + private fun deserialize(json: String): SentryMetricsEvents { + val reader = JsonObjectReader(StringReader(json)) + return SentryMetricsEvents.Deserializer().deserialize(reader, fixture.logger) + } + + companion object { + data class Point(val x: Int, val y: Int) { + override fun toString(): String = "Point{x:$x,y:$y}-Hello" + } + } +} diff --git a/sentry/src/test/resources/json/sentry_metrics.json b/sentry/src/test/resources/json/sentry_metrics.json new file mode 100644 index 0000000000..4bacc7ecec --- /dev/null +++ b/sentry/src/test/resources/json/sentry_metrics.json @@ -0,0 +1,59 @@ +{ + "items": + [ + { + "timestamp": 1081621443.000000, + "type": "counter", + "name": "42e6bd2a-c45e-414d-8066-ed5196fbc686", + "value": 123.0, + "trace_id": "5c1f73d39486827b9e60ceb1fc23277a", + "span_id": "f28b86350e534671", + "unit": "visit", + "attributes": + { + "sentry.sdk.name": + { + "type": "string", + "value": "sentry.java.spring-boot.jakarta" + }, + "sentry.environment": + { + "type": "string", + "value": "production" + }, + "sentry.sdk.version": + { + "type": "string", + "value": "8.11.1" + }, + "sentry.trace.parent_span_id": + { + "type": "string", + "value": "f28b86350e534671" + }, + "custom.boolean": + { + "type": "boolean", + "value": true + }, + "custom.point2": { + "type": "string", + "value": "Point{x:21,y:31}-Hello" + }, + "custom.double": { + "type": "double", + "value": 11.12 + }, + "custom.point": { + "type": "string", + "value": "Point{x:20,y:30}-Hello" + }, + "custom.integer": + { + "type": "integer", + "value": 10 + } + } + } + ] +} From d3cadef473a9fde8e996d23d1a6cec6a4ecc9c17 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 5 Jan 2026 14:11:19 +0100 Subject: [PATCH 20/40] Create SentryMetricsParameters for Metrics --- sentry/api/sentry.api | 30 +++++++---- .../java/io/sentry/metrics/IMetricsApi.java | 7 ++- .../java/io/sentry/metrics/MetricsApi.java | 27 +++++----- .../io/sentry/metrics/NoOpMetricsApi.java | 7 ++- .../metrics/SentryMetricsParameters.java | 52 +++++++++++++++++++ sentry/src/test/java/io/sentry/ScopesTest.kt | 21 ++++---- 6 files changed, 103 insertions(+), 41 deletions(-) create mode 100644 sentry/src/main/java/io/sentry/metrics/SentryMetricsParameters.java diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 23561b1dba..f0ca49b500 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -5211,14 +5211,14 @@ public abstract interface class io/sentry/metrics/IMetricsApi { public abstract fun count (Ljava/lang/String;)V public abstract fun count (Ljava/lang/String;Ljava/lang/Double;)V public abstract fun count (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;)V - public abstract fun count (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/logger/SentryLogParameters;)V + public abstract fun count (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/metrics/SentryMetricsParameters;)V public abstract fun count (Ljava/lang/String;Ljava/lang/String;)V public abstract fun distribution (Ljava/lang/String;Ljava/lang/Double;)V public abstract fun distribution (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;)V - public abstract fun distribution (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/logger/SentryLogParameters;)V + public abstract fun distribution (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/metrics/SentryMetricsParameters;)V public abstract fun gauge (Ljava/lang/String;Ljava/lang/Double;)V public abstract fun gauge (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;)V - public abstract fun gauge (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/logger/SentryLogParameters;)V + public abstract fun gauge (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/metrics/SentryMetricsParameters;)V } public abstract interface class io/sentry/metrics/IMetricsBatchProcessor { @@ -5236,14 +5236,14 @@ public final class io/sentry/metrics/MetricsApi : io/sentry/metrics/IMetricsApi public fun count (Ljava/lang/String;)V public fun count (Ljava/lang/String;Ljava/lang/Double;)V public fun count (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;)V - public fun count (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/logger/SentryLogParameters;)V + public fun count (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/metrics/SentryMetricsParameters;)V public fun count (Ljava/lang/String;Ljava/lang/String;)V public fun distribution (Ljava/lang/String;Ljava/lang/Double;)V public fun distribution (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;)V - public fun distribution (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/logger/SentryLogParameters;)V + public fun distribution (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/metrics/SentryMetricsParameters;)V public fun gauge (Ljava/lang/String;Ljava/lang/Double;)V public fun gauge (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;)V - public fun gauge (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/logger/SentryLogParameters;)V + public fun gauge (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/metrics/SentryMetricsParameters;)V } public class io/sentry/metrics/MetricsBatchProcessor : io/sentry/metrics/IMetricsBatchProcessor { @@ -5261,14 +5261,14 @@ public final class io/sentry/metrics/NoOpMetricsApi : io/sentry/metrics/IMetrics public fun count (Ljava/lang/String;)V public fun count (Ljava/lang/String;Ljava/lang/Double;)V public fun count (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;)V - public fun count (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/logger/SentryLogParameters;)V + public fun count (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/metrics/SentryMetricsParameters;)V public fun count (Ljava/lang/String;Ljava/lang/String;)V public fun distribution (Ljava/lang/String;Ljava/lang/Double;)V public fun distribution (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;)V - public fun distribution (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/logger/SentryLogParameters;)V + public fun distribution (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/metrics/SentryMetricsParameters;)V public fun gauge (Ljava/lang/String;Ljava/lang/Double;)V public fun gauge (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;)V - public fun gauge (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/logger/SentryLogParameters;)V + public fun gauge (Ljava/lang/String;Ljava/lang/Double;Ljava/lang/String;Lio/sentry/metrics/SentryMetricsParameters;)V public static fun getInstance ()Lio/sentry/metrics/NoOpMetricsApi; } @@ -5279,6 +5279,18 @@ public final class io/sentry/metrics/NoOpMetricsBatchProcessor : io/sentry/metri public static fun getInstance ()Lio/sentry/metrics/NoOpMetricsBatchProcessor; } +public final class io/sentry/metrics/SentryMetricsParameters { + public fun ()V + public static fun create (Lio/sentry/SentryAttributes;)Lio/sentry/metrics/SentryMetricsParameters; + public static fun create (Lio/sentry/SentryDate;Lio/sentry/SentryAttributes;)Lio/sentry/metrics/SentryMetricsParameters; + public fun getAttributes ()Lio/sentry/SentryAttributes; + public fun getOrigin ()Ljava/lang/String; + public fun getTimestamp ()Lio/sentry/SentryDate; + public fun setAttributes (Lio/sentry/SentryAttributes;)V + public fun setOrigin (Ljava/lang/String;)V + public fun setTimestamp (Lio/sentry/SentryDate;)V +} + public final class io/sentry/opentelemetry/OpenTelemetryUtil { public fun ()V public static fun applyIgnoredSpanOrigins (Lio/sentry/SentryOptions;)V diff --git a/sentry/src/main/java/io/sentry/metrics/IMetricsApi.java b/sentry/src/main/java/io/sentry/metrics/IMetricsApi.java index e61ec3dc21..e03b41e173 100644 --- a/sentry/src/main/java/io/sentry/metrics/IMetricsApi.java +++ b/sentry/src/main/java/io/sentry/metrics/IMetricsApi.java @@ -1,6 +1,5 @@ package io.sentry.metrics; -import io.sentry.logger.SentryLogParameters; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -18,7 +17,7 @@ void count( final @NotNull String name, final @Nullable Double value, final @Nullable String unit, - final @NotNull SentryLogParameters params); + final @NotNull SentryMetricsParameters params); void distribution(final @NotNull String name, final @Nullable Double value); @@ -29,7 +28,7 @@ void distribution( final @NotNull String name, final @Nullable Double value, final @Nullable String unit, - final @NotNull SentryLogParameters params); + final @NotNull SentryMetricsParameters params); void gauge(final @NotNull String name, final @Nullable Double value); @@ -39,5 +38,5 @@ void gauge( final @NotNull String name, final @Nullable Double value, final @Nullable String unit, - final @NotNull SentryLogParameters params); + final @NotNull SentryMetricsParameters params); } diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java index 894bd7aba2..2afbccb3f9 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java @@ -14,7 +14,6 @@ import io.sentry.SentryMetricsEvent; import io.sentry.SentryOptions; import io.sentry.SpanId; -import io.sentry.logger.SentryLogParameters; import io.sentry.protocol.SdkVersion; import io.sentry.protocol.SentryId; import io.sentry.protocol.User; @@ -34,23 +33,23 @@ public MetricsApi(final @NotNull Scopes scopes) { @Override public void count(final @NotNull String name) { - captureMetrics(SentryLogParameters.create(null, null), name, "counter", 1.0, null); + captureMetrics(SentryMetricsParameters.create(null, null), name, "counter", 1.0, null); } @Override public void count(final @NotNull String name, final @Nullable Double value) { - captureMetrics(SentryLogParameters.create(null, null), name, "counter", value, null); + captureMetrics(SentryMetricsParameters.create(null, null), name, "counter", value, null); } @Override public void count(final @NotNull String name, final @Nullable String unit) { - captureMetrics(SentryLogParameters.create(null, null), name, "counter", 1.0, unit); + captureMetrics(SentryMetricsParameters.create(null, null), name, "counter", 1.0, unit); } @Override public void count( final @NotNull String name, final @Nullable Double value, final @Nullable String unit) { - captureMetrics(SentryLogParameters.create(null, null), name, "counter", value, unit); + captureMetrics(SentryMetricsParameters.create(null, null), name, "counter", value, unit); } @Override @@ -58,19 +57,19 @@ public void count( final @NotNull String name, final @Nullable Double value, final @Nullable String unit, - final @NotNull SentryLogParameters params) { + final @NotNull SentryMetricsParameters params) { captureMetrics(params, name, "counter", value, unit); } @Override public void distribution(final @NotNull String name, final @Nullable Double value) { - captureMetrics(SentryLogParameters.create(null, null), name, "distribution", value, null); + captureMetrics(SentryMetricsParameters.create(null, null), name, "distribution", value, null); } @Override public void distribution( final @NotNull String name, final @Nullable Double value, final @Nullable String unit) { - captureMetrics(SentryLogParameters.create(null, null), name, "distribution", value, unit); + captureMetrics(SentryMetricsParameters.create(null, null), name, "distribution", value, unit); } @Override @@ -78,19 +77,19 @@ public void distribution( final @NotNull String name, final @Nullable Double value, final @Nullable String unit, - final @NotNull SentryLogParameters params) { + final @NotNull SentryMetricsParameters params) { captureMetrics(params, name, "distribution", value, unit); } @Override public void gauge(final @NotNull String name, final @Nullable Double value) { - captureMetrics(SentryLogParameters.create(null, null), name, "gauge", value, null); + captureMetrics(SentryMetricsParameters.create(null, null), name, "gauge", value, null); } @Override public void gauge( final @NotNull String name, final @Nullable Double value, final @Nullable String unit) { - captureMetrics(SentryLogParameters.create(null, null), name, "gauge", value, unit); + captureMetrics(SentryMetricsParameters.create(null, null), name, "gauge", value, unit); } @Override @@ -98,13 +97,13 @@ public void gauge( final @NotNull String name, final @Nullable Double value, final @Nullable String unit, - final @NotNull SentryLogParameters params) { + final @NotNull SentryMetricsParameters params) { captureMetrics(params, name, "gauge", value, unit); } @SuppressWarnings("AnnotateFormatMethod") private void captureMetrics( - final @NotNull SentryLogParameters params, + final @NotNull SentryMetricsParameters params, final @Nullable String name, final @Nullable String type, final @Nullable Double value, @@ -166,7 +165,7 @@ private void captureMetrics( } private @NotNull HashMap createAttributes( - final @NotNull SentryLogParameters params) { + final @NotNull SentryMetricsParameters params) { final @NotNull HashMap attributes = new HashMap<>(); final @NotNull String origin = params.getOrigin(); if (!"manual".equalsIgnoreCase(origin)) { diff --git a/sentry/src/main/java/io/sentry/metrics/NoOpMetricsApi.java b/sentry/src/main/java/io/sentry/metrics/NoOpMetricsApi.java index 6e770b249c..7fe85145d4 100644 --- a/sentry/src/main/java/io/sentry/metrics/NoOpMetricsApi.java +++ b/sentry/src/main/java/io/sentry/metrics/NoOpMetricsApi.java @@ -1,6 +1,5 @@ package io.sentry.metrics; -import io.sentry.logger.SentryLogParameters; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -31,7 +30,7 @@ public void count( final @NotNull String name, final @Nullable Double value, final @Nullable String unit, - final @NotNull SentryLogParameters params) {} + final @NotNull SentryMetricsParameters params) {} @Override public void distribution(final @NotNull String name, final @Nullable Double value) {} @@ -45,7 +44,7 @@ public void distribution( final @NotNull String name, final @Nullable Double value, final @Nullable String unit, - final @NotNull SentryLogParameters params) {} + final @NotNull SentryMetricsParameters params) {} @Override public void gauge(final @NotNull String name, final @Nullable Double value) {} @@ -59,5 +58,5 @@ public void gauge( final @NotNull String name, final @Nullable Double value, final @Nullable String unit, - final @NotNull SentryLogParameters params) {} + final @NotNull SentryMetricsParameters params) {} } diff --git a/sentry/src/main/java/io/sentry/metrics/SentryMetricsParameters.java b/sentry/src/main/java/io/sentry/metrics/SentryMetricsParameters.java new file mode 100644 index 0000000000..14d8403c20 --- /dev/null +++ b/sentry/src/main/java/io/sentry/metrics/SentryMetricsParameters.java @@ -0,0 +1,52 @@ +package io.sentry.metrics; + +import io.sentry.SentryAttributes; +import io.sentry.SentryDate; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class SentryMetricsParameters { + + private @Nullable SentryDate timestamp; + private @Nullable SentryAttributes attributes; + private @NotNull String origin = "manual"; + + public @Nullable SentryDate getTimestamp() { + return timestamp; + } + + public void setTimestamp(final @Nullable SentryDate timestamp) { + this.timestamp = timestamp; + } + + public @Nullable SentryAttributes getAttributes() { + return attributes; + } + + public void setAttributes(final @Nullable SentryAttributes attributes) { + this.attributes = attributes; + } + + public @NotNull String getOrigin() { + return origin; + } + + public void setOrigin(final @NotNull String origin) { + this.origin = origin; + } + + public static @NotNull SentryMetricsParameters create( + final @Nullable SentryDate timestamp, final @Nullable SentryAttributes attributes) { + final @NotNull SentryMetricsParameters params = new SentryMetricsParameters(); + + params.setTimestamp(timestamp); + params.setAttributes(attributes); + + return params; + } + + public static @NotNull SentryMetricsParameters create( + final @Nullable SentryAttributes attributes) { + return create(null, attributes); + } +} diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index dde633d878..fc17a30094 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -8,6 +8,7 @@ import io.sentry.clientreport.DiscardedEvent import io.sentry.hints.SessionEndHint import io.sentry.hints.SessionStartHint import io.sentry.logger.SentryLogParameters +import io.sentry.metrics.SentryMetricsParameters import io.sentry.protocol.Feedback import io.sentry.protocol.SentryId import io.sentry.protocol.SentryTransaction @@ -3217,7 +3218,7 @@ class ScopesTest { sut .metrics() - .count("metric name", 1.0, "visit", SentryLogParameters().also { it.origin = "other" }) + .count("metric name", 1.0, "visit", SentryMetricsParameters().also { it.origin = "other" }) verify(mockClient) .captureMetric( @@ -3295,7 +3296,7 @@ class ScopesTest { "metric name", 1.0, "visit", - SentryLogParameters.create(SentryAttributes.fromMap(mapOf("attrname1" to "attrval1"))), + SentryMetricsParameters.create(SentryAttributes.fromMap(mapOf("attrname1" to "attrval1"))), ) verify(mockClient) @@ -3324,7 +3325,7 @@ class ScopesTest { "metric name", 1.0, "visit", - SentryLogParameters.create( + SentryMetricsParameters.create( SentryAttributes.of( SentryAttribute.stringAttribute("strattr", "strval"), SentryAttribute.booleanAttribute("boolattr", true), @@ -3392,7 +3393,7 @@ class ScopesTest { "metric name", 1.0, "visit", - SentryLogParameters.create( + SentryMetricsParameters.create( SentryLongDate(123), SentryAttributes.of(SentryAttribute.named("attrname1", "attrval1")), ), @@ -3459,7 +3460,7 @@ class ScopesTest { "metric name", 3.7, "ms", - SentryLogParameters.create(SentryAttributes.fromMap(mapOf("attrname1" to "attrval1"))), + SentryMetricsParameters.create(SentryAttributes.fromMap(mapOf("attrname1" to "attrval1"))), ) verify(mockClient) @@ -3488,7 +3489,7 @@ class ScopesTest { "metric name", 3.7, "ms", - SentryLogParameters.create( + SentryMetricsParameters.create( SentryAttributes.of( SentryAttribute.stringAttribute("strattr", "strval"), SentryAttribute.booleanAttribute("boolattr", true), @@ -3556,7 +3557,7 @@ class ScopesTest { "metric name", 3.7, "ms", - SentryLogParameters.create( + SentryMetricsParameters.create( SentryLongDate(123), SentryAttributes.of(SentryAttribute.named("attrname1", "attrval1")), ), @@ -3623,7 +3624,7 @@ class ScopesTest { "metric name", 256.0, "byte", - SentryLogParameters.create(SentryAttributes.fromMap(mapOf("attrname1" to "attrval1"))), + SentryMetricsParameters.create(SentryAttributes.fromMap(mapOf("attrname1" to "attrval1"))), ) verify(mockClient) @@ -3652,7 +3653,7 @@ class ScopesTest { "metric name", 256.0, "byte", - SentryLogParameters.create( + SentryMetricsParameters.create( SentryAttributes.of( SentryAttribute.stringAttribute("strattr", "strval"), SentryAttribute.booleanAttribute("boolattr", true), @@ -3720,7 +3721,7 @@ class ScopesTest { "metric name", 256.0, "byte", - SentryLogParameters.create( + SentryMetricsParameters.create( SentryLongDate(123), SentryAttributes.of(SentryAttribute.named("attrname1", "attrval1")), ), From 75beb0812fd45edff4aca68d196c201afdc51318 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 8 Jan 2026 11:30:19 +0100 Subject: [PATCH 21/40] Use uv for managing python version --- .envrc | 6 ++++-- .python-version | 1 + devenv/config.ini | 16 ++++++++++++++++ devenv/sync.py | 20 ++++++++++++++++++++ project.toml | 3 +++ 5 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 .python-version create mode 100644 devenv/config.ini create mode 100644 devenv/sync.py create mode 100644 project.toml diff --git a/.envrc b/.envrc index 97b3f16c6f..7488c24611 100644 --- a/.envrc +++ b/.envrc @@ -1,2 +1,4 @@ -export VIRTUAL_ENV=".venv" -layout python3 +export VIRTUAL_ENV="${PWD}/.venv" + +PATH_add "${PWD}/.venv/bin" + diff --git a/.python-version b/.python-version new file mode 100644 index 0000000000..2c20ac9bea --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13.3 diff --git a/devenv/config.ini b/devenv/config.ini new file mode 100644 index 0000000000..b546b762c0 --- /dev/null +++ b/devenv/config.ini @@ -0,0 +1,16 @@ +[devenv] +minimum_version = 1.22.1 + +[uv] +darwin_arm64 = https://github.com/astral-sh/uv/releases/download/0.8.2/uv-aarch64-apple-darwin.tar.gz +darwin_arm64_sha256 = 954d24634d5f37fa26c7af75eb79893d11623fc81b4de4b82d60d1ade4bfca22 +darwin_x86_64 = https://github.com/astral-sh/uv/releases/download/0.8.2/uv-x86_64-apple-darwin.tar.gz +darwin_x86_64_sha256 = ae755df53c8c2c1f3dfbee6e3d2e00be0dfbc9c9b4bdffdb040b96f43678b7ce +linux_arm64 = https://github.com/astral-sh/uv/releases/download/0.8.2/uv-aarch64-unknown-linux-gnu.tar.gz +linux_arm64_sha256 = 27da35ef54e9131c2e305de67dd59a07c19257882c6b1f3cf4d8d5fbb8eaf4ca +linux_x86_64 = https://github.com/astral-sh/uv/releases/download/0.8.2/uv-x86_64-unknown-linux-gnu.tar.gz +linux_x86_64_sha256 = 6dcb28a541868a455aefb2e8d4a1283dd6bf888605a2db710f0530cec888b0ad +# used for autoupdate +# NOTE: if using uv-build as a build backend, you'll have to make sure the versions match +version = 0.8.2 + diff --git a/devenv/sync.py b/devenv/sync.py new file mode 100644 index 0000000000..9a5a4167b8 --- /dev/null +++ b/devenv/sync.py @@ -0,0 +1,20 @@ +from devenv import constants +from devenv.lib import config, proc, uv + +def main(context: dict[str, str]) -> int: + reporoot = context["reporoot"] + cfg = config.get_repo(reporoot) + + uv.install( + cfg["uv"]["version"], + cfg["uv"][constants.SYSTEM_MACHINE], + cfg["uv"][f"{constants.SYSTEM_MACHINE}_sha256"], + reporoot, + ) + + # reporoot/.venv is the default venv location + print(f"syncing .venv ...") + proc.run(("uv", "sync", "--frozen", "--quiet")) + + return 0 + diff --git a/project.toml b/project.toml new file mode 100644 index 0000000000..55509e3912 --- /dev/null +++ b/project.toml @@ -0,0 +1,3 @@ +[project] +name = "javasdk" +version = "0.0.0" From 924a40c2a57706df13c31aed6ecb5b1c6bec9e5f Mon Sep 17 00:00:00 2001 From: lcian <17258265+lcian@users.noreply.github.com> Date: Thu, 8 Jan 2026 15:01:18 +0100 Subject: [PATCH 22/40] fix --- .envrc | 3 +-- .gitignore | 1 + project.toml => pyproject.toml | 0 uv.lock | 8 ++++++++ 4 files changed, 10 insertions(+), 2 deletions(-) rename project.toml => pyproject.toml (100%) create mode 100644 uv.lock diff --git a/.envrc b/.envrc index 7488c24611..f58a7cee60 100644 --- a/.envrc +++ b/.envrc @@ -1,4 +1,3 @@ export VIRTUAL_ENV="${PWD}/.venv" - +devenv sync PATH_add "${PWD}/.venv/bin" - diff --git a/.gitignore b/.gitignore index be4f11ce3d..5f87fdbd5c 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ spring-server.txt spy.log .kotlin **/tomcat.8080/webapps/ +**/__pycache__ diff --git a/project.toml b/pyproject.toml similarity index 100% rename from project.toml rename to pyproject.toml diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000000..84b7527660 --- /dev/null +++ b/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 2 +requires-python = ">=3.13" + +[[package]] +name = "javasdk" +version = "0.0.0" +source = { virtual = "." } From 301f9a2e93dcc34d3b31c7b9695678c6c133318b Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 7 Jan 2026 10:08:26 +0100 Subject: [PATCH 23/40] Add device and OS attributes to metrics --- .../core/DefaultAndroidEventProcessor.java | 35 ++++++++++++++ .../core/DefaultAndroidEventProcessorTest.kt | 46 +++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java index dd4baed463..7db570195e 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java @@ -93,6 +93,13 @@ public DefaultAndroidEventProcessor( return event; } + @Override + public @Nullable SentryMetricsEvent process(@NotNull SentryMetricsEvent event) { + setDevice(event); + setOs(event); + return event; + } + /** * The last exception is usually used for picking the issue title, but the convention is to send * inner exceptions first, e.g. [inner, outer] This doesn't work very well on Android, as some @@ -248,6 +255,34 @@ private void setOs(final @NotNull SentryLogEvent event) { } } + private void setDevice(final @NotNull SentryMetricsEvent event) { + try { + event.setAttribute( + "device.brand", + new SentryLogEventAttributeValue(SentryAttributeType.STRING, Build.BRAND)); + event.setAttribute( + "device.model", + new SentryLogEventAttributeValue(SentryAttributeType.STRING, Build.MODEL)); + event.setAttribute( + "device.family", + new SentryLogEventAttributeValue(SentryAttributeType.STRING, deviceFamily.getValue())); + } catch (Throwable e) { + options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve device info", e); + } + } + + private void setOs(final @NotNull SentryMetricsEvent event) { + try { + event.setAttribute( + "os.name", new SentryLogEventAttributeValue(SentryAttributeType.STRING, "Android")); + event.setAttribute( + "os.version", + new SentryLogEventAttributeValue(SentryAttributeType.STRING, Build.VERSION.RELEASE)); + } catch (Throwable e) { + options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve os system", e); + } + } + // Data to be applied to events that was created in the running process private void processNonCachedEvent( final @NotNull SentryBaseEvent event, final @NotNull Hint hint) { diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt index 32e82155b2..ee27fed740 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt @@ -5,11 +5,15 @@ import android.os.Build import android.os.Looper import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.sentry.DateUtils import io.sentry.DiagnosticLogger import io.sentry.Hint import io.sentry.IScopes import io.sentry.SentryEvent import io.sentry.SentryLevel +import io.sentry.SentryLogEvent +import io.sentry.SentryLogLevel +import io.sentry.SentryMetricsEvent import io.sentry.SentryTracer import io.sentry.TransactionContext import io.sentry.TypeCheckHint.SENTRY_DART_SDK_NAME @@ -17,6 +21,7 @@ import io.sentry.android.core.internal.util.CpuInfoUtils import io.sentry.protocol.OperatingSystem import io.sentry.protocol.SdkVersion import io.sentry.protocol.SentryException +import io.sentry.protocol.SentryId import io.sentry.protocol.SentryStackFrame import io.sentry.protocol.SentryStackTrace import io.sentry.protocol.SentryThread @@ -619,4 +624,45 @@ class DefaultAndroidEventProcessorTest { assertEquals("IllegalArgumentException", it.exceptions!![1].type) } } + + @Test + fun `device and os are set on metric`() { + val sut = fixture.getSut(context) + val processedEvent: SentryMetricsEvent? = + sut.process( + SentryMetricsEvent( + SentryId("5c1f73d39486827b9e60ceb1fc23277a"), + DateUtils.dateToSeconds(DateUtils.getDateTime("2004-04-10T18:24:03.000Z")), + "42e6bd2a-c45e-414d-8066-ed5196fbc686", + "counter", + 123.0, + ) + ) + + assertNotNull(processedEvent?.attributes?.get("device.brand")) + assertNotNull(processedEvent?.attributes?.get("device.model")) + assertNotNull(processedEvent?.attributes?.get("device.family")) + assertNotNull(processedEvent?.attributes?.get("os.name")) + assertNotNull(processedEvent?.attributes?.get("os.version")) + } + + @Test + fun `device and os are set on log`() { + val sut = fixture.getSut(context) + val processedEvent: SentryLogEvent? = + sut.process( + SentryLogEvent( + SentryId("5c1f73d39486827b9e60ceb1fc23277a"), + DateUtils.dateToSeconds(DateUtils.getDateTime("2004-04-10T18:24:03.000Z")), + "message", + SentryLogLevel.WARN, + ) + ) + + assertNotNull(processedEvent?.attributes?.get("device.brand")) + assertNotNull(processedEvent?.attributes?.get("device.model")) + assertNotNull(processedEvent?.attributes?.get("device.family")) + assertNotNull(processedEvent?.attributes?.get("os.name")) + assertNotNull(processedEvent?.attributes?.get("os.version")) + } } From af4f8c42e8d84a0d8a479bb1096e5845e306e5ac Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 7 Jan 2026 12:38:23 +0100 Subject: [PATCH 24/40] Add AI rules files for Metrics (and update feature flags rule) --- .cursor/rules/feature_flags.mdc | 26 +++++++++++++++++++++++--- .cursor/rules/metrics.mdc | 26 ++++++++++++++++++++++++++ .cursor/rules/overview_dev.mdc | 14 +++++++++++++- .cursor/rules/scopes.mdc | 9 +++++++++ 4 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 .cursor/rules/metrics.mdc diff --git a/.cursor/rules/feature_flags.mdc b/.cursor/rules/feature_flags.mdc index 6ced606429..f2a78bc71c 100644 --- a/.cursor/rules/feature_flags.mdc +++ b/.cursor/rules/feature_flags.mdc @@ -10,15 +10,35 @@ There is a scope based and a span based API for tracking feature flag evaluation The `addFeatureFlag` method can be used to track feature flag evaluations. It exists on `Sentry` static API as well as `IScopes` and `IScope`. +When using static API, `IScopes` or COMBINED scope type, Sentry will also invoke `addFeatureFlag` on the current span. This does not happen, when directly invoking `addFeatureFlag` on `IScope` (except for COMBINED scope type). + The `maxFeatureFlags` option controls how many flags are tracked per scope and also how many are sent to Sentry as part of events. Scope based feature flags can also be disabled by setting the value to 0. Defaults to 100 feature flag evaluations. Order of feature flag evaluations is important as we only keep track of the last {maxFeatureFlag} items. -When a feature flag evluation with the same name is added, the previous one is removed and the new one is stored so that it'll be dropped last. +When a feature flag evaluation with the same name is added, the previous one is removed and the new one is stored so that it'll be dropped last. +Refer to `FeatureFlagBuffer` fore more details. `FeatureFlagBuffer` has been optimized for storing scope based feature flag evaluations, especially clone performance. -When sending out an error event, feature flag buffers from all three scope types (global, isolation and current scope) are merged, chosing the newest {maxFeatureFlag} entries across all scope types. Feature flags are sent as part of the `flags` context. +When sending out an error event, feature flag buffers from all three scope types (global, isolation and current scope) are merged, choosing the newest {maxFeatureFlag} entries across all scope types. Feature flags are sent as part of the `flags` context. ## Span Based API -tbd +It's also possible to use the `addFeatureFlag` method on `ISpan` (and by extension `ITransaction`). Feature flag evaluations tracked this way +will not be added to the scope and thus won't be added to error events. + +Each span has its own `SpanFeatureFlagBuffer`. When starting a child span, feature flag evaluations are NOT copied from the parent. Each span starts out with an empty buffer and has its own limit. +`SpanFeatureFlagBuffer` has been optimized for storing feature flag evaluations on spans. + +Spans have a hard coded limit of 10 feature flag evaluations. When full, new entries are rejected. Updates to existing entries are still allowed even if full. + +## Integrations + +We offer integrations that automatically track feature flag evaluations. + +Android: +- LaunchDarkly (`SentryLaunchDarklyAndroidHook`) + +JVM (non Android): +- LaunchDarkly (`SentryLaunchDarklyServerHook`) +- OpenFeature (`SentryOpenFeatureHook`) diff --git a/.cursor/rules/metrics.mdc b/.cursor/rules/metrics.mdc new file mode 100644 index 0000000000..8d5c803fa4 --- /dev/null +++ b/.cursor/rules/metrics.mdc @@ -0,0 +1,26 @@ +--- +alwaysApply: false +description: Metrics +--- +# Java SDK Metrics + +Metrics are enabled by default. + +API has been namespaced under `Sentry.metrics()` and `IScopes.metrics()` using the `IMetricsApi` interface and `MetricsApi` implementation. + +Options are namespaced under `SentryOptions.getMetrics()`. + +Three different APIs exist: +- `count`: Counters are one of the more basic types of metrics and can be used to count certain event occurrences. +- `distribution`: Distributions help you get the most insights from your data by allowing you to obtain aggregations such as p90, min, max, and avg. +- `gauge`: Gauges let you obtain aggregates like min, max, avg, sum, and count. They can be represented in a more space-efficient way than distributions, but they can't be used to get percentiles. If percentiles aren't important to you, we recommend using gauges. + +Refer to `SentryMetricsEvent` for details about available fields. + +`MetricsBatchProcessor` handles batching (`MAX_BATCH_SIZE`), automatic sending of metrics after a timeout (`FLUSH_AFTER_MS`) and rejecting if `MAX_QUEUE_SIZE` has been hit. + +The flow is `IMetricsApi` -> `IMetricsBatchProcessor` -> `SentryClient.captureBatchedMetricsEvents` -> `ITransport`. + +Each `SentryMetricsEvent` goes through `SentryOptions.metrics.beforeSend` (if configured) and can be modified or dropped. + +For sending, a batch of `SentryMetricsEvent` objects is sent inside a `SentryMetricsEvents` object. diff --git a/.cursor/rules/overview_dev.mdc b/.cursor/rules/overview_dev.mdc index 12ac73a844..f05d2992d4 100644 --- a/.cursor/rules/overview_dev.mdc +++ b/.cursor/rules/overview_dev.mdc @@ -37,10 +37,21 @@ Use the `fetch_rules` tool to include these rules when working on specific areas - **`feature_flags`**: Use when working with: - Feature flag tracking and evaluation - `addFeatureFlag()`, `getFeatureFlags()` methods - - `FeatureFlagBuffer`, `FeatureFlag` protocol + - `FeatureFlagBuffer`, `SpanFeatureFlagBuffer`, `FeatureFlag` protocol - `maxFeatureFlags` option and buffer management - Feature flag merging across scope types - Scope-based vs span-based feature flag APIs + - Scope-based API: `Sentry`, `IScopes`, `IScope` APIs + - Span-based API: `ISpan`, `ITransaction` APIs + - Integrations: LaunchDarkly (Android/JVM), OpenFeature (JVM) + +- **`metrics`**: Use when working with: + - Metrics API (`Sentry.metrics()`, `IScopes.metrics()`) + - `IMetricsApi`, `MetricsApi` implementation + - Metrics types: `count`, `distribution`, `gauge` + - `MetricsBatchProcessor`, batching and queue management + - `SentryMetricsEvent`, `SentryMetricsEvents` + - `SentryOptions.getMetrics()`, `beforeSend` callback ### Integration & Infrastructure - **`opentelemetry`**: Use when working with: @@ -72,3 +83,4 @@ Use the `fetch_rules` tool to include these rules when working on specific areas - Cache/offline/network → `offline` - System test/e2e/sample → `e2e_tests` - Feature flag/addFeatureFlag/flag evaluation → `feature_flags` + - Metrics/count/distribution/gauge → `metrics` diff --git a/.cursor/rules/scopes.mdc b/.cursor/rules/scopes.mdc index 179b094356..e054755d4f 100644 --- a/.cursor/rules/scopes.mdc +++ b/.cursor/rules/scopes.mdc @@ -49,6 +49,15 @@ Data is also passed on to newly forked child scopes but not to parents. Current scope can be retrieved from `Scopes` via `getScope`. +### Combined Scope + +This is a special scope type that combines global, isolation and current scope. + +Refer to `CombinedScopeView` for each field of interest to see whether values from the three individual scopes are merged, +whether a specific one is used or whether we're simply using the first one that has a value. + +Also see the section about `defaultScopeType` further down. + ## Storage of `Scopes` `Scopes` are stored in a `ThreadLocal` by default (NOTE: this is different for OpenTelemetry, see opentelemetry.mdc). From 8330a3edb18d2eda9320879e54e048da1baa6a84 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 8 Jan 2026 14:00:52 +0100 Subject: [PATCH 25/40] Close and flush MetricsBatchProcessor from SentryClient --- .../src/main/java/io/sentry/SentryClient.java | 2 ++ .../test/java/io/sentry/SentryClientTest.kt | 22 +++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index e67a6a6aea..f198de5342 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -1657,6 +1657,7 @@ public void close(final boolean isRestarting) { try { flush(isRestarting ? 0 : options.getShutdownTimeoutMillis()); loggerBatchProcessor.close(isRestarting); + metricsBatchProcessor.close(isRestarting); transport.close(isRestarting); } catch (IOException e) { options @@ -1684,6 +1685,7 @@ public void close(final boolean isRestarting) { @Override public void flush(final long timeoutMillis) { loggerBatchProcessor.flush(timeoutMillis); + metricsBatchProcessor.flush(timeoutMillis); transport.flush(timeoutMillis); } diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 28d176d85b..ef7c8e7b08 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -14,7 +14,9 @@ import io.sentry.hints.Cached import io.sentry.hints.DiskFlushNotification import io.sentry.hints.TransactionEnd import io.sentry.logger.ILoggerBatchProcessor +import io.sentry.logger.ILoggerBatchProcessorFactory import io.sentry.metrics.IMetricsBatchProcessor +import io.sentry.metrics.IMetricsBatchProcessorFactory import io.sentry.protocol.Contexts import io.sentry.protocol.Feedback import io.sentry.protocol.Mechanism @@ -78,6 +80,10 @@ class SentryClientTest { class Fixture { var transport = mock() var factory = mock() + var loggerBatchProcessor = mock() + var loggerBatchProcessorFactory = mock() + var metricsBatchProcessor = mock() + var metricsBatchProcessorFactory = mock() val maxAttachmentSize: Long = (5 * 1024 * 1024).toLong() val scopes = mock() val sentryTracer: SentryTracer @@ -94,12 +100,16 @@ class SentryClientTest { setLogger(mock()) maxAttachmentSize = this@Fixture.maxAttachmentSize setTransportFactory(factory) + logs.setLoggerBatchProcessorFactory(loggerBatchProcessorFactory) + metrics.setMetricsBatchProcessorFactory(metricsBatchProcessorFactory) release = "0.0.1" isTraceSampling = true } init { whenever(factory.create(any(), any())).thenReturn(transport) + whenever(loggerBatchProcessorFactory.create(any(), any())).thenReturn(loggerBatchProcessor) + whenever(metricsBatchProcessorFactory.create(any(), any())).thenReturn(metricsBatchProcessor) whenever(scopes.options).thenReturn(sentryOptions) sentryTracer = SentryTracer( @@ -168,21 +178,29 @@ class SentryClientTest { @Test fun `when client is closed with isRestarting false, transport waits`() { - val sut = fixture.getSut() + val sut = fixture.getSut { options -> options.logs.isEnabled = true } assertTrue(sut.isEnabled) sut.close(false) assertNotEquals(0, fixture.sentryOptions.shutdownTimeoutMillis) verify(fixture.transport).flush(eq(fixture.sentryOptions.shutdownTimeoutMillis)) + verify(fixture.loggerBatchProcessor).flush(eq(fixture.sentryOptions.shutdownTimeoutMillis)) + verify(fixture.metricsBatchProcessor).flush(eq(fixture.sentryOptions.shutdownTimeoutMillis)) verify(fixture.transport).close(eq(false)) + verify(fixture.loggerBatchProcessor).close(eq(false)) + verify(fixture.metricsBatchProcessor).close(eq(false)) } @Test fun `when client is closed with isRestarting true, transport does not wait`() { - val sut = fixture.getSut() + val sut = fixture.getSut { options -> options.logs.isEnabled = true } assertTrue(sut.isEnabled) sut.close(true) verify(fixture.transport).flush(eq(0)) + verify(fixture.loggerBatchProcessor).flush(eq(0)) + verify(fixture.metricsBatchProcessor).flush(eq(0)) verify(fixture.transport).close(eq(true)) + verify(fixture.loggerBatchProcessor).close(eq(true)) + verify(fixture.metricsBatchProcessor).close(eq(true)) } @Test From 53ea75b3befa2b127c101555492690dd120b6c5a Mon Sep 17 00:00:00 2001 From: lcian <17258265+lcian@users.noreply.github.com> Date: Thu, 8 Jan 2026 15:05:14 +0100 Subject: [PATCH 26/40] fix --- devenv/sync.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/devenv/sync.py b/devenv/sync.py index 9a5a4167b8..45e663cd99 100644 --- a/devenv/sync.py +++ b/devenv/sync.py @@ -1,5 +1,6 @@ from devenv import constants from devenv.lib import config, proc, uv +import os def main(context: dict[str, str]) -> int: reporoot = context["reporoot"] @@ -14,6 +15,8 @@ def main(context: dict[str, str]) -> int: # reporoot/.venv is the default venv location print(f"syncing .venv ...") + if not os.path.exists(".venv"): + proc.run(("uv", "venv", "--seed")) proc.run(("uv", "sync", "--frozen", "--quiet")) return 0 From 9988b7ebbc53223b7cff9a790bd8e87b2757386c Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 8 Jan 2026 14:15:05 +0100 Subject: [PATCH 27/40] Add E2E tests for Metrics --- .../java/io/sentry/samples/console/Main.java | 8 +++ .../ConsoleApplicationSystemTest.kt | 6 ++ .../java/io/sentry/samples/console/Main.java | 8 +++ .../ConsoleApplicationSystemTest.kt | 6 ++ .../samples/spring7/web/MetricController.java | 33 +++++++++++ .../io/sentry/systemtest/MetricsSystemTest.kt | 55 +++++++++++++++++++ .../spring/boot4/MetricController.java | 33 +++++++++++ .../io/sentry/systemtest/MetricsSystemTest.kt | 55 +++++++++++++++++++ .../spring/boot4/MetricController.java | 33 +++++++++++ .../io/sentry/systemtest/MetricsSystemTest.kt | 55 +++++++++++++++++++ .../spring/boot4/MetricController.java | 33 +++++++++++ .../io/sentry/systemtest/MetricsSystemTest.kt | 55 +++++++++++++++++++ .../spring/boot4/MetricController.java | 33 +++++++++++ .../io/sentry/systemtest/MetricsSystemTest.kt | 55 +++++++++++++++++++ .../spring/boot/jakarta/MetricController.java | 33 +++++++++++ .../io/sentry/systemtest/MetricsSystemTest.kt | 55 +++++++++++++++++++ .../spring/boot/jakarta/MetricController.java | 33 +++++++++++ .../io/sentry/systemtest/MetricsSystemTest.kt | 55 +++++++++++++++++++ .../spring/boot/jakarta/MetricController.java | 33 +++++++++++ .../io/sentry/systemtest/MetricsSystemTest.kt | 55 +++++++++++++++++++ .../samples/spring/boot/MetricController.java | 33 +++++++++++ .../io/sentry/systemtest/MetricsSystemTest.kt | 55 +++++++++++++++++++ .../samples/spring/boot/MetricController.java | 33 +++++++++++ .../io/sentry/systemtest/MetricsSystemTest.kt | 55 +++++++++++++++++++ .../spring/boot/jakarta/MetricController.java | 33 +++++++++++ .../io/sentry/systemtest/MetricsSystemTest.kt | 55 +++++++++++++++++++ .../samples/spring/boot/MetricController.java | 33 +++++++++++ .../io/sentry/systemtest/MetricsSystemTest.kt | 55 +++++++++++++++++++ .../samples/spring/boot/MetricController.java | 33 +++++++++++ .../io/sentry/systemtest/MetricsSystemTest.kt | 55 +++++++++++++++++++ .../spring/jakarta/web/MetricController.java | 33 +++++++++++ .../io/sentry/systemtest/MetricsSystemTest.kt | 55 +++++++++++++++++++ .../samples/spring/web/MetricController.java | 33 +++++++++++ .../io/sentry/systemtest/MetricsSystemTest.kt | 55 +++++++++++++++++++ .../api/sentry-system-test-support.api | 5 ++ .../util/LoggingInsecureRestClient.kt | 3 + .../sentry/systemtest/util/RestTestClient.kt | 18 ++++++ .../io/sentry/systemtest/util/TestHelper.kt | 49 +++++++++++++++++ 38 files changed, 1423 insertions(+) create mode 100644 sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/MetricController.java create mode 100644 sentry-samples/sentry-samples-spring-7/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/MetricController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/MetricController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/MetricController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/MetricController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/MetricController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-opentelemetry/src/main/java/io/sentry/samples/spring/boot/MetricController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-webflux/src/main/java/io/sentry/samples/spring/boot/MetricController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-webflux/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/MetricController.java create mode 100644 sentry-samples/sentry-samples-spring-boot/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/MetricController.java create mode 100644 sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring/src/main/java/io/sentry/samples/spring/web/MetricController.java create mode 100644 sentry-samples/sentry-samples-spring/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt diff --git a/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/main/java/io/sentry/samples/console/Main.java b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/main/java/io/sentry/samples/console/Main.java index a41e853e15..9efab21031 100644 --- a/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/main/java/io/sentry/samples/console/Main.java +++ b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/main/java/io/sentry/samples/console/Main.java @@ -63,6 +63,8 @@ public static void main(String[] args) throws InterruptedException { Sentry.addFeatureFlag("my-feature-flag", true); + captureMetrics(); + // Sending exception: Exception exception = new RuntimeException("Some error!"); Sentry.captureException(exception); @@ -136,6 +138,12 @@ public static void main(String[] args) throws InterruptedException { // Sentry.close(); } + private static void captureMetrics() { + Sentry.metrics().count("countMetric"); + Sentry.metrics().gauge("gaugeMetric", 5.0); + Sentry.metrics().distribution("distributionMetric", 7.0); + } + private static class SomeEventProcessor implements EventProcessor { @Override public SentryEvent process(SentryEvent event, Hint hint) { diff --git a/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt index 29d144355a..537c631983 100644 --- a/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt +++ b/sentry-samples/sentry-samples-console-opentelemetry-noagent/src/test/kotlin/sentry/systemtest/ConsoleApplicationSystemTest.kt @@ -97,5 +97,11 @@ class ConsoleApplicationSystemTest { breadcrumb.message?.contains("Processed by") == true } == true } + + testHelper.ensureMetricsReceived { metricsEvents, sentryEnvelopeHeader -> + testHelper.doesContainMetric(metricsEvents, "countMetric", "counter", 1.0) && + testHelper.doesContainMetric(metricsEvents, "gaugeMetric", "gauge", 5.0) && + testHelper.doesContainMetric(metricsEvents, "distributionMetric", "distribution", 7.0) + } } } diff --git a/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java b/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java index 5f557b7ad2..fd21476f40 100644 --- a/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java +++ b/sentry-samples/sentry-samples-console/src/main/java/io/sentry/samples/console/Main.java @@ -128,6 +128,8 @@ public static void main(String[] args) throws InterruptedException { Sentry.addFeatureFlag("my-feature-flag", true); + captureMetrics(); + // Sending exception: Exception exception = new RuntimeException("Some error!"); Sentry.captureException(exception); @@ -187,6 +189,12 @@ public static void main(String[] args) throws InterruptedException { // Sentry.close(); } + private static void captureMetrics() { + Sentry.metrics().count("countMetric"); + Sentry.metrics().gauge("gaugeMetric", 5.0); + Sentry.metrics().distribution("distributionMetric", 7.0); + } + private static class SomeEventProcessor implements EventProcessor { @Override public SentryEvent process(SentryEvent event, Hint hint) { diff --git a/sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt b/sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt index cf09728047..6b75a942f5 100644 --- a/sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt +++ b/sentry-samples/sentry-samples-console/src/test/kotlin/io/sentry/systemtest/ConsoleApplicationSystemTest.kt @@ -93,5 +93,11 @@ class ConsoleApplicationSystemTest { breadcrumb.message?.contains("Processed by") == true } == true } + + testHelper.ensureMetricsReceived { metricsEvents, sentryEnvelopeHeader -> + testHelper.doesContainMetric(metricsEvents, "countMetric", "counter", 1.0) && + testHelper.doesContainMetric(metricsEvents, "gaugeMetric", "gauge", 5.0) && + testHelper.doesContainMetric(metricsEvents, "distributionMetric", "distribution", 7.0) + } } } diff --git a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/MetricController.java b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/MetricController.java new file mode 100644 index 0000000000..cc8386fde2 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/MetricController.java @@ -0,0 +1,33 @@ +package io.sentry.samples.spring7.web; + +import io.sentry.Sentry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/metric/") +public class MetricController { + private static final Logger LOGGER = LoggerFactory.getLogger(MetricController.class); + + @GetMapping("count") + String count() { + Sentry.metrics().count("countMetric"); + return "count metric increased"; + } + + @GetMapping("gauge/{count}") + String gauge(@PathVariable Long count) { + Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + return "gauge metric tracked"; + } + + @GetMapping("distribution/{count}") + String distribution(@PathVariable Long count) { + Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + return "distribution metric tracked"; + } +} diff --git a/sentry-samples/sentry-samples-spring-7/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-7/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt new file mode 100644 index 0000000000..a379681adb --- /dev/null +++ b/sentry-samples/sentry-samples-spring-7/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -0,0 +1,55 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class MetricsSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `count metric`() { + val restClient = testHelper.restClient + assertEquals("count metric increased", restClient.getCountMetric()) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) + } + } + + @Test + fun `gauge metric`() { + val restClient = testHelper.restClient + assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) + } + } + + @Test + fun `distribution metric`() { + val restClient = testHelper.restClient + assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) + } + } +} 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 new file mode 100644 index 0000000000..f9163349a9 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/MetricController.java @@ -0,0 +1,33 @@ +package io.sentry.samples.spring.boot4; + +import io.sentry.Sentry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/metric/") +public class MetricController { + private static final Logger LOGGER = LoggerFactory.getLogger(MetricController.class); + + @GetMapping("count") + String count() { + Sentry.metrics().count("countMetric"); + return "count metric increased"; + } + + @GetMapping("gauge/{count}") + String gauge(@PathVariable Long count) { + Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + return "gauge metric tracked"; + } + + @GetMapping("distribution/{count}") + String distribution(@PathVariable Long count) { + Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + return "distribution metric tracked"; + } +} 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 new file mode 100644 index 0000000000..a379681adb --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -0,0 +1,55 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class MetricsSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `count metric`() { + val restClient = testHelper.restClient + assertEquals("count metric increased", restClient.getCountMetric()) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) + } + } + + @Test + fun `gauge metric`() { + val restClient = testHelper.restClient + assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) + } + } + + @Test + fun `distribution metric`() { + val restClient = testHelper.restClient + assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) + } + } +} 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 new file mode 100644 index 0000000000..f9163349a9 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/MetricController.java @@ -0,0 +1,33 @@ +package io.sentry.samples.spring.boot4; + +import io.sentry.Sentry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/metric/") +public class MetricController { + private static final Logger LOGGER = LoggerFactory.getLogger(MetricController.class); + + @GetMapping("count") + String count() { + Sentry.metrics().count("countMetric"); + return "count metric increased"; + } + + @GetMapping("gauge/{count}") + String gauge(@PathVariable Long count) { + Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + return "gauge metric tracked"; + } + + @GetMapping("distribution/{count}") + String distribution(@PathVariable Long count) { + Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + return "distribution metric tracked"; + } +} 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 new file mode 100644 index 0000000000..a379681adb --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -0,0 +1,55 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class MetricsSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `count metric`() { + val restClient = testHelper.restClient + assertEquals("count metric increased", restClient.getCountMetric()) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) + } + } + + @Test + fun `gauge metric`() { + val restClient = testHelper.restClient + assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) + } + } + + @Test + fun `distribution metric`() { + val restClient = testHelper.restClient + assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) + } + } +} 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 new file mode 100644 index 0000000000..f9163349a9 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/MetricController.java @@ -0,0 +1,33 @@ +package io.sentry.samples.spring.boot4; + +import io.sentry.Sentry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/metric/") +public class MetricController { + private static final Logger LOGGER = LoggerFactory.getLogger(MetricController.class); + + @GetMapping("count") + String count() { + Sentry.metrics().count("countMetric"); + return "count metric increased"; + } + + @GetMapping("gauge/{count}") + String gauge(@PathVariable Long count) { + Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + return "gauge metric tracked"; + } + + @GetMapping("distribution/{count}") + String distribution(@PathVariable Long count) { + Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + return "distribution metric tracked"; + } +} 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 new file mode 100644 index 0000000000..a379681adb --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -0,0 +1,55 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class MetricsSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `count metric`() { + val restClient = testHelper.restClient + assertEquals("count metric increased", restClient.getCountMetric()) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) + } + } + + @Test + fun `gauge metric`() { + val restClient = testHelper.restClient + assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) + } + } + + @Test + fun `distribution metric`() { + val restClient = testHelper.restClient + assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) + } + } +} 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 new file mode 100644 index 0000000000..f9163349a9 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/MetricController.java @@ -0,0 +1,33 @@ +package io.sentry.samples.spring.boot4; + +import io.sentry.Sentry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/metric/") +public class MetricController { + private static final Logger LOGGER = LoggerFactory.getLogger(MetricController.class); + + @GetMapping("count") + String count() { + Sentry.metrics().count("countMetric"); + return "count metric increased"; + } + + @GetMapping("gauge/{count}") + String gauge(@PathVariable Long count) { + Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + return "gauge metric tracked"; + } + + @GetMapping("distribution/{count}") + String distribution(@PathVariable Long count) { + Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + return "distribution metric tracked"; + } +} 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 new file mode 100644 index 0000000000..a379681adb --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -0,0 +1,55 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class MetricsSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `count metric`() { + val restClient = testHelper.restClient + assertEquals("count metric increased", restClient.getCountMetric()) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) + } + } + + @Test + fun `gauge metric`() { + val restClient = testHelper.restClient + assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) + } + } + + @Test + fun `distribution metric`() { + val restClient = testHelper.restClient + assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java new file mode 100644 index 0000000000..8c6203c09e --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java @@ -0,0 +1,33 @@ +package io.sentry.samples.spring.boot.jakarta; + +import io.sentry.Sentry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/metric/") +public class MetricController { + private static final Logger LOGGER = LoggerFactory.getLogger(MetricController.class); + + @GetMapping("count") + String count() { + Sentry.metrics().count("countMetric"); + return "count metric increased"; + } + + @GetMapping("gauge/{count}") + String gauge(@PathVariable Long count) { + Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + return "gauge metric tracked"; + } + + @GetMapping("distribution/{count}") + String distribution(@PathVariable Long count) { + Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + return "distribution metric tracked"; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt new file mode 100644 index 0000000000..a379681adb --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -0,0 +1,55 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class MetricsSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `count metric`() { + val restClient = testHelper.restClient + assertEquals("count metric increased", restClient.getCountMetric()) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) + } + } + + @Test + fun `gauge metric`() { + val restClient = testHelper.restClient + assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) + } + } + + @Test + fun `distribution metric`() { + val restClient = testHelper.restClient + assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java new file mode 100644 index 0000000000..8c6203c09e --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java @@ -0,0 +1,33 @@ +package io.sentry.samples.spring.boot.jakarta; + +import io.sentry.Sentry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/metric/") +public class MetricController { + private static final Logger LOGGER = LoggerFactory.getLogger(MetricController.class); + + @GetMapping("count") + String count() { + Sentry.metrics().count("countMetric"); + return "count metric increased"; + } + + @GetMapping("gauge/{count}") + String gauge(@PathVariable Long count) { + Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + return "gauge metric tracked"; + } + + @GetMapping("distribution/{count}") + String distribution(@PathVariable Long count) { + Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + return "distribution metric tracked"; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt new file mode 100644 index 0000000000..a379681adb --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -0,0 +1,55 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class MetricsSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `count metric`() { + val restClient = testHelper.restClient + assertEquals("count metric increased", restClient.getCountMetric()) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) + } + } + + @Test + fun `gauge metric`() { + val restClient = testHelper.restClient + assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) + } + } + + @Test + fun `distribution metric`() { + val restClient = testHelper.restClient + assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java new file mode 100644 index 0000000000..8c6203c09e --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java @@ -0,0 +1,33 @@ +package io.sentry.samples.spring.boot.jakarta; + +import io.sentry.Sentry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/metric/") +public class MetricController { + private static final Logger LOGGER = LoggerFactory.getLogger(MetricController.class); + + @GetMapping("count") + String count() { + Sentry.metrics().count("countMetric"); + return "count metric increased"; + } + + @GetMapping("gauge/{count}") + String gauge(@PathVariable Long count) { + Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + return "gauge metric tracked"; + } + + @GetMapping("distribution/{count}") + String distribution(@PathVariable Long count) { + Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + return "distribution metric tracked"; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt new file mode 100644 index 0000000000..a379681adb --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -0,0 +1,55 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class MetricsSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `count metric`() { + val restClient = testHelper.restClient + assertEquals("count metric increased", restClient.getCountMetric()) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) + } + } + + @Test + fun `gauge metric`() { + val restClient = testHelper.restClient + assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) + } + } + + @Test + fun `distribution metric`() { + val restClient = testHelper.restClient + assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/MetricController.java b/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/MetricController.java new file mode 100644 index 0000000000..2f1dabf784 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/MetricController.java @@ -0,0 +1,33 @@ +package io.sentry.samples.spring.boot; + +import io.sentry.Sentry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/metric/") +public class MetricController { + private static final Logger LOGGER = LoggerFactory.getLogger(MetricController.class); + + @GetMapping("count") + String count() { + Sentry.metrics().count("countMetric"); + return "count metric increased"; + } + + @GetMapping("gauge/{count}") + String gauge(@PathVariable Long count) { + Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + return "gauge metric tracked"; + } + + @GetMapping("distribution/{count}") + String distribution(@PathVariable Long count) { + Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + return "distribution metric tracked"; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt new file mode 100644 index 0000000000..a379681adb --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -0,0 +1,55 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class MetricsSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `count metric`() { + val restClient = testHelper.restClient + assertEquals("count metric increased", restClient.getCountMetric()) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) + } + } + + @Test + fun `gauge metric`() { + val restClient = testHelper.restClient + assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) + } + } + + @Test + fun `distribution metric`() { + val restClient = testHelper.restClient + assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-opentelemetry/src/main/java/io/sentry/samples/spring/boot/MetricController.java b/sentry-samples/sentry-samples-spring-boot-opentelemetry/src/main/java/io/sentry/samples/spring/boot/MetricController.java new file mode 100644 index 0000000000..2f1dabf784 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-opentelemetry/src/main/java/io/sentry/samples/spring/boot/MetricController.java @@ -0,0 +1,33 @@ +package io.sentry.samples.spring.boot; + +import io.sentry.Sentry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/metric/") +public class MetricController { + private static final Logger LOGGER = LoggerFactory.getLogger(MetricController.class); + + @GetMapping("count") + String count() { + Sentry.metrics().count("countMetric"); + return "count metric increased"; + } + + @GetMapping("gauge/{count}") + String gauge(@PathVariable Long count) { + Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + return "gauge metric tracked"; + } + + @GetMapping("distribution/{count}") + String distribution(@PathVariable Long count) { + Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + return "distribution metric tracked"; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt new file mode 100644 index 0000000000..a379681adb --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -0,0 +1,55 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class MetricsSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `count metric`() { + val restClient = testHelper.restClient + assertEquals("count metric increased", restClient.getCountMetric()) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) + } + } + + @Test + fun `gauge metric`() { + val restClient = testHelper.restClient + assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) + } + } + + @Test + fun `distribution metric`() { + val restClient = testHelper.restClient + assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java new file mode 100644 index 0000000000..8c6203c09e --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java @@ -0,0 +1,33 @@ +package io.sentry.samples.spring.boot.jakarta; + +import io.sentry.Sentry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/metric/") +public class MetricController { + private static final Logger LOGGER = LoggerFactory.getLogger(MetricController.class); + + @GetMapping("count") + String count() { + Sentry.metrics().count("countMetric"); + return "count metric increased"; + } + + @GetMapping("gauge/{count}") + String gauge(@PathVariable Long count) { + Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + return "gauge metric tracked"; + } + + @GetMapping("distribution/{count}") + String distribution(@PathVariable Long count) { + Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + return "distribution metric tracked"; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt new file mode 100644 index 0000000000..a379681adb --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -0,0 +1,55 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class MetricsSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `count metric`() { + val restClient = testHelper.restClient + assertEquals("count metric increased", restClient.getCountMetric()) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) + } + } + + @Test + fun `gauge metric`() { + val restClient = testHelper.restClient + assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) + } + } + + @Test + fun `distribution metric`() { + val restClient = testHelper.restClient + assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-webflux/src/main/java/io/sentry/samples/spring/boot/MetricController.java b/sentry-samples/sentry-samples-spring-boot-webflux/src/main/java/io/sentry/samples/spring/boot/MetricController.java new file mode 100644 index 0000000000..2f1dabf784 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-webflux/src/main/java/io/sentry/samples/spring/boot/MetricController.java @@ -0,0 +1,33 @@ +package io.sentry.samples.spring.boot; + +import io.sentry.Sentry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/metric/") +public class MetricController { + private static final Logger LOGGER = LoggerFactory.getLogger(MetricController.class); + + @GetMapping("count") + String count() { + Sentry.metrics().count("countMetric"); + return "count metric increased"; + } + + @GetMapping("gauge/{count}") + String gauge(@PathVariable Long count) { + Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + return "gauge metric tracked"; + } + + @GetMapping("distribution/{count}") + String distribution(@PathVariable Long count) { + Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + return "distribution metric tracked"; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-webflux/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-webflux/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt new file mode 100644 index 0000000000..a379681adb --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-webflux/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -0,0 +1,55 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class MetricsSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `count metric`() { + val restClient = testHelper.restClient + assertEquals("count metric increased", restClient.getCountMetric()) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) + } + } + + @Test + fun `gauge metric`() { + val restClient = testHelper.restClient + assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) + } + } + + @Test + fun `distribution metric`() { + val restClient = testHelper.restClient + assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/MetricController.java b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/MetricController.java new file mode 100644 index 0000000000..2f1dabf784 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/MetricController.java @@ -0,0 +1,33 @@ +package io.sentry.samples.spring.boot; + +import io.sentry.Sentry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/metric/") +public class MetricController { + private static final Logger LOGGER = LoggerFactory.getLogger(MetricController.class); + + @GetMapping("count") + String count() { + Sentry.metrics().count("countMetric"); + return "count metric increased"; + } + + @GetMapping("gauge/{count}") + String gauge(@PathVariable Long count) { + Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + return "gauge metric tracked"; + } + + @GetMapping("distribution/{count}") + String distribution(@PathVariable Long count) { + Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + return "distribution metric tracked"; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-boot/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt new file mode 100644 index 0000000000..a379681adb --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -0,0 +1,55 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class MetricsSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `count metric`() { + val restClient = testHelper.restClient + assertEquals("count metric increased", restClient.getCountMetric()) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) + } + } + + @Test + fun `gauge metric`() { + val restClient = testHelper.restClient + assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) + } + } + + @Test + fun `distribution metric`() { + val restClient = testHelper.restClient + assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/MetricController.java b/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/MetricController.java new file mode 100644 index 0000000000..f2e609b0f8 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/MetricController.java @@ -0,0 +1,33 @@ +package io.sentry.samples.spring.jakarta.web; + +import io.sentry.Sentry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/metric/") +public class MetricController { + private static final Logger LOGGER = LoggerFactory.getLogger(MetricController.class); + + @GetMapping("count") + String count() { + Sentry.metrics().count("countMetric"); + return "count metric increased"; + } + + @GetMapping("gauge/{count}") + String gauge(@PathVariable Long count) { + Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + return "gauge metric tracked"; + } + + @GetMapping("distribution/{count}") + String distribution(@PathVariable Long count) { + Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + return "distribution metric tracked"; + } +} diff --git a/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt new file mode 100644 index 0000000000..a379681adb --- /dev/null +++ b/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -0,0 +1,55 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class MetricsSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `count metric`() { + val restClient = testHelper.restClient + assertEquals("count metric increased", restClient.getCountMetric()) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) + } + } + + @Test + fun `gauge metric`() { + val restClient = testHelper.restClient + assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) + } + } + + @Test + fun `distribution metric`() { + val restClient = testHelper.restClient + assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) + } + } +} diff --git a/sentry-samples/sentry-samples-spring/src/main/java/io/sentry/samples/spring/web/MetricController.java b/sentry-samples/sentry-samples-spring/src/main/java/io/sentry/samples/spring/web/MetricController.java new file mode 100644 index 0000000000..b425f973a6 --- /dev/null +++ b/sentry-samples/sentry-samples-spring/src/main/java/io/sentry/samples/spring/web/MetricController.java @@ -0,0 +1,33 @@ +package io.sentry.samples.spring.web; + +import io.sentry.Sentry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/metric/") +public class MetricController { + private static final Logger LOGGER = LoggerFactory.getLogger(MetricController.class); + + @GetMapping("count") + String count() { + Sentry.metrics().count("countMetric"); + return "count metric increased"; + } + + @GetMapping("gauge/{count}") + String gauge(@PathVariable Long count) { + Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + return "gauge metric tracked"; + } + + @GetMapping("distribution/{count}") + String distribution(@PathVariable Long count) { + Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + return "distribution metric tracked"; + } +} diff --git a/sentry-samples/sentry-samples-spring/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt new file mode 100644 index 0000000000..a379681adb --- /dev/null +++ b/sentry-samples/sentry-samples-spring/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -0,0 +1,55 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class MetricsSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `count metric`() { + val restClient = testHelper.restClient + assertEquals("count metric increased", restClient.getCountMetric()) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) + } + } + + @Test + fun `gauge metric`() { + val restClient = testHelper.restClient + assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) + } + } + + @Test + fun `distribution metric`() { + val restClient = testHelper.restClient + assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) + assertEquals(200, restClient.lastKnownStatusCode) + + Thread.sleep(10000) + + testHelper.ensureMetricsReceived { event, header -> + testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) + } + } +} 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 1f5d1382a9..406b246a82 100644 --- a/sentry-system-test-support/api/sentry-system-test-support.api +++ b/sentry-system-test-support/api/sentry-system-test-support.api @@ -549,6 +549,9 @@ public final class io/sentry/systemtest/util/RestTestClient : io/sentry/systemte public final fun createPersonDistributedTracing (Lio/sentry/systemtest/Person;Ljava/util/Map;)Lio/sentry/systemtest/Person; public static synthetic fun createPersonDistributedTracing$default (Lio/sentry/systemtest/util/RestTestClient;Lio/sentry/systemtest/Person;Ljava/util/Map;ILjava/lang/Object;)Lio/sentry/systemtest/Person; public final fun errorWithFeatureFlag (Ljava/lang/String;)Ljava/lang/String; + public final fun getCountMetric ()Ljava/lang/String; + public final fun getDistributionMetric (J)Ljava/lang/String; + public final fun getGaugeMetric (J)Ljava/lang/String; public final fun getPerson (J)Lio/sentry/systemtest/Person; public final fun getPersonDistributedTracing (JLjava/util/Map;)Lio/sentry/systemtest/Person; public static synthetic fun getPersonDistributedTracing$default (Lio/sentry/systemtest/util/RestTestClient;JLjava/util/Map;ILjava/lang/Object;)Lio/sentry/systemtest/Person; @@ -567,6 +570,7 @@ public final class io/sentry/systemtest/util/SentryMockServerClient : io/sentry/ public final class io/sentry/systemtest/util/TestHelper { public fun (Ljava/lang/String;)V public final fun doesContainLogWithBody (Lio/sentry/SentryLogEvents;Ljava/lang/String;)Z + public final fun doesContainMetric (Lio/sentry/SentryMetricsEvents;Ljava/lang/String;Ljava/lang/String;D)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 doesTransactionContainSpanWithDescription (Lio/sentry/protocol/SentryTransaction;Ljava/lang/String;)Z @@ -584,6 +588,7 @@ public final class io/sentry/systemtest/util/TestHelper { public final fun ensureErrorCount (Lcom/apollographql/apollo3/api/ApolloResponse;I)V public final fun ensureErrorReceived (Lkotlin/jvm/functions/Function1;)V public final fun ensureLogsReceived (Lkotlin/jvm/functions/Function2;)V + public final fun ensureMetricsReceived (Lkotlin/jvm/functions/Function2;)V public final fun ensureNoEnvelopeReceived (Lkotlin/jvm/functions/Function1;)V public final fun ensureNoErrors (Lcom/apollographql/apollo3/api/ApolloResponse;)V public final fun ensureNoTransactionReceived (Lkotlin/jvm/functions/Function2;)V diff --git a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/LoggingInsecureRestClient.kt b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/LoggingInsecureRestClient.kt index c93a7dd938..8c4f27345f 100644 --- a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/LoggingInsecureRestClient.kt +++ b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/LoggingInsecureRestClient.kt @@ -26,6 +26,9 @@ open class LoggingInsecureRestClient { if (response?.isSuccessful != true) { return null } + if (T::class == String::class) { + return responseBody as? T + } return responseBody?.let { objectMapper().readValue(it, T::class.java) } } diff --git a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt index cb097d4483..695c853891 100644 --- a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt +++ b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt @@ -62,6 +62,24 @@ class RestTestClient(private val backendBaseUrl: String) : LoggingInsecureRestCl val response = call(request, true) return response?.body?.string() } + + fun getCountMetric(): String? { + val request = Request.Builder().url("$backendBaseUrl/metric/count") + + return callTyped(request, true) + } + + fun getGaugeMetric(count: Long): String? { + val request = Request.Builder().url("$backendBaseUrl/metric/gauge/$count") + + return callTyped(request, true) + } + + fun getDistributionMetric(count: Long): String? { + val request = Request.Builder().url("$backendBaseUrl/metric/distribution/$count") + + return callTyped(request, true) + } } data class FeatureFlagResponse(val flagKey: String, val value: Boolean) 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 5a334ba243..d6a2c3470d 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 @@ -7,6 +7,7 @@ import io.sentry.SentryEnvelopeHeader import io.sentry.SentryEvent import io.sentry.SentryItemType import io.sentry.SentryLogEvents +import io.sentry.SentryMetricsEvents import io.sentry.SentryOptions import io.sentry.protocol.FeatureFlag import io.sentry.protocol.SentrySpan @@ -118,6 +119,54 @@ class TestHelper(backendUrl: String) { return callback(logs, envelopeHeader) } + fun doesContainMetric( + metrics: SentryMetricsEvents, + name: String, + type: String, + value: Double, + ): Boolean { + val metricItem = + metrics.items.firstOrNull { metricItem -> + metricItem.name == name && metricItem.type == type && metricItem.value == value + } + if (metricItem == null) { + println("Unable to find metric item with name $name, type $type and value $value in metrics:") + logObject(metrics) + return false + } + + return true + } + + fun ensureMetricsReceived(callback: ((SentryMetricsEvents, SentryEnvelopeHeader) -> Boolean)) { + ensureEnvelopeReceived { envelopeString -> checkIfMetricsMatch(envelopeString, callback) } + } + + private fun checkIfMetricsMatch( + envelopeString: String, + callback: ((SentryMetricsEvents, SentryEnvelopeHeader) -> Boolean), + ): Boolean { + val deserializeEnvelope = jsonSerializer.deserializeEnvelope(envelopeString.byteInputStream()) + if (deserializeEnvelope == null) { + return false + } + + val envelopeHeader = deserializeEnvelope.header + + val metricsItem = + deserializeEnvelope.items.firstOrNull { it.header.type == SentryItemType.TraceMetric } + if (metricsItem == null) { + return false + } + + val metrics = metricsItem.getMetrics(jsonSerializer) + if (metrics == null) { + return false + } + + return callback(metrics, envelopeHeader) + } + fun doesContainLogWithBody(logs: SentryLogEvents, body: String): Boolean { val logItem = logs.items.firstOrNull { logItem -> logItem.body == body } if (logItem == null) { From f502ffeeafe0f3294d36fdbf5e3396c73f4e14c3 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 8 Jan 2026 15:21:51 +0100 Subject: [PATCH 28/40] Final keyword in Metrics methods --- .../core/AndroidMetricsBatchProcessor.java | 2 +- .../AndroidMetricsBatchProcessorFactory.java | 2 +- .../main/java/io/sentry/SentryLogEvent.java | 22 +++++++------- .../sentry/SentryLogEventAttributeValue.java | 4 +-- .../main/java/io/sentry/SentryLogEvents.java | 4 +-- .../java/io/sentry/SentryMetricsEvent.java | 30 +++++++++---------- .../java/io/sentry/SentryMetricsEvents.java | 4 +-- .../main/java/io/sentry/SentryOptions.java | 6 ++-- .../DefaultMetricsBatchProcessorFactory.java | 2 +- .../metrics/IMetricsBatchProcessor.java | 2 +- .../sentry/metrics/MetricsBatchProcessor.java | 5 ++++ 11 files changed, 44 insertions(+), 39 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessor.java index 64c612a886..290f2a9d4e 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessor.java @@ -12,7 +12,7 @@ public final class AndroidMetricsBatchProcessor extends MetricsBatchProcessor implements AppState.AppStateListener { public AndroidMetricsBatchProcessor( - @NotNull SentryOptions options, @NotNull ISentryClient client) { + final @NotNull SentryOptions options, final @NotNull ISentryClient client) { super(options, client); AppState.getInstance().addAppStateListener(this); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessorFactory.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessorFactory.java index eef0a6311f..319440c27a 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessorFactory.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessorFactory.java @@ -9,7 +9,7 @@ public final class AndroidMetricsBatchProcessorFactory implements IMetricsBatchProcessorFactory { @Override public @NotNull IMetricsBatchProcessor create( - @NotNull SentryOptions options, @NotNull SentryClient client) { + final @NotNull SentryOptions options, final @NotNull SentryClient client) { return new AndroidMetricsBatchProcessor(options, client); } } diff --git a/sentry/src/main/java/io/sentry/SentryLogEvent.java b/sentry/src/main/java/io/sentry/SentryLogEvent.java index 2afcf57a1d..441e1ef79a 100644 --- a/sentry/src/main/java/io/sentry/SentryLogEvent.java +++ b/sentry/src/main/java/io/sentry/SentryLogEvent.java @@ -53,7 +53,7 @@ public void setTimestamp(final @NotNull Double timestamp) { return body; } - public void setBody(@NotNull String body) { + public void setBody(final @NotNull String body) { this.body = body; } @@ -88,7 +88,7 @@ public void setAttribute( return severityNumber; } - public void setSeverityNumber(@Nullable Integer severityNumber) { + public void setSeverityNumber(final @Nullable Integer severityNumber) { this.severityNumber = severityNumber; } @@ -120,7 +120,7 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger if (unknown != null) { for (String key : unknown.keySet()) { - Object value = unknown.get(key); + final Object value = unknown.get(key); writer.name(key).value(logger, value); } } @@ -185,29 +185,29 @@ public static final class Deserializer implements JsonDeserializer= MAX_QUEUE_SIZE) { options .getClientReportRecorder() @@ -65,6 +69,7 @@ public void add(final @NotNull SentryMetricsEvent metricsEvent) { @SuppressWarnings("FutureReturnValueIgnored") @Override public void close(final boolean isRestarting) { + isShuttingDown = true; if (isRestarting) { maybeSchedule(true, true); executorService.submit(() -> executorService.close(options.getShutdownTimeoutMillis())); From 76e7b2cc4a1892c1f1d49c582a014ee27695a556 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 12 Jan 2026 10:16:53 +0100 Subject: [PATCH 29/40] Add Hint to Metrics --- .../core/DefaultAndroidEventProcessor.java | 3 +- .../core/DefaultAndroidEventProcessorTest.kt | 3 +- .../core/SessionTrackingIntegrationTest.kt | 2 +- sentry/api/sentry.api | 10 +++--- .../main/java/io/sentry/EventProcessor.java | 2 +- .../main/java/io/sentry/ISentryClient.java | 3 +- .../main/java/io/sentry/NoOpSentryClient.java | 3 +- .../src/main/java/io/sentry/SentryClient.java | 26 +++++++++----- .../main/java/io/sentry/SentryOptions.java | 3 +- .../java/io/sentry/metrics/MetricsApi.java | 2 +- .../metrics/SentryMetricsParameters.java | 11 ++++++ sentry/src/test/java/io/sentry/ScopesTest.kt | 35 +++++++++++++++++-- .../test/java/io/sentry/SentryClientTest.kt | 13 ++++--- 13 files changed, 89 insertions(+), 27 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java index 7db570195e..5e72ad7cfc 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java @@ -94,7 +94,8 @@ public DefaultAndroidEventProcessor( } @Override - public @Nullable SentryMetricsEvent process(@NotNull SentryMetricsEvent event) { + public @Nullable SentryMetricsEvent process( + final @NotNull SentryMetricsEvent event, final @NotNull Hint hint) { setDevice(event); setOs(event); return event; diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt index ee27fed740..2810cb3b00 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt @@ -636,7 +636,8 @@ class DefaultAndroidEventProcessorTest { "42e6bd2a-c45e-414d-8066-ed5196fbc686", "counter", 123.0, - ) + ), + Hint(), ) assertNotNull(processedEvent?.attributes?.get("device.brand")) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt index 1a2f0db30c..ef920e1d7f 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt @@ -194,7 +194,7 @@ class SessionTrackingIntegrationTest { TODO("Not yet implemented") } - override fun captureMetric(event: SentryMetricsEvent, scope: IScope?) { + override fun captureMetric(event: SentryMetricsEvent, scope: IScope?, hint: Hint?) { TODO("Not yet implemented") } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index f0ca49b500..1f0783721e 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -461,7 +461,7 @@ public abstract interface class io/sentry/EventProcessor { public fun getOrder ()Ljava/lang/Long; public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; public fun process (Lio/sentry/SentryLogEvent;)Lio/sentry/SentryLogEvent; - public fun process (Lio/sentry/SentryMetricsEvent;)Lio/sentry/SentryMetricsEvent; + public fun process (Lio/sentry/SentryMetricsEvent;Lio/sentry/Hint;)Lio/sentry/SentryMetricsEvent; public fun process (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/SentryReplayEvent; public fun process (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction; } @@ -1048,7 +1048,7 @@ public abstract interface class io/sentry/ISentryClient { public abstract fun captureLog (Lio/sentry/SentryLogEvent;Lio/sentry/IScope;)V public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; - public abstract fun captureMetric (Lio/sentry/SentryMetricsEvent;Lio/sentry/IScope;)V + public abstract fun captureMetric (Lio/sentry/SentryMetricsEvent;Lio/sentry/IScope;Lio/sentry/Hint;)V public abstract fun captureProfileChunk (Lio/sentry/ProfileChunk;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; public abstract fun captureReplayEvent (Lio/sentry/SentryReplayEvent;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureSession (Lio/sentry/Session;)V @@ -2863,7 +2863,7 @@ public final class io/sentry/SentryClient : io/sentry/ISentryClient { public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureFeedback (Lio/sentry/protocol/Feedback;Lio/sentry/Hint;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; public fun captureLog (Lio/sentry/SentryLogEvent;Lio/sentry/IScope;)V - public fun captureMetric (Lio/sentry/SentryMetricsEvent;Lio/sentry/IScope;)V + public fun captureMetric (Lio/sentry/SentryMetricsEvent;Lio/sentry/IScope;Lio/sentry/Hint;)V public fun captureProfileChunk (Lio/sentry/ProfileChunk;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; public fun captureReplayEvent (Lio/sentry/SentryReplayEvent;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureSession (Lio/sentry/Session;Lio/sentry/Hint;)V @@ -3769,7 +3769,7 @@ public final class io/sentry/SentryOptions$Metrics { } public abstract interface class io/sentry/SentryOptions$Metrics$BeforeSendMetricCallback { - public abstract fun execute (Lio/sentry/SentryMetricsEvent;)Lio/sentry/SentryMetricsEvent; + public abstract fun execute (Lio/sentry/SentryMetricsEvent;Lio/sentry/Hint;)Lio/sentry/SentryMetricsEvent; } public abstract interface class io/sentry/SentryOptions$OnDiscardCallback { @@ -5284,9 +5284,11 @@ public final class io/sentry/metrics/SentryMetricsParameters { public static fun create (Lio/sentry/SentryAttributes;)Lio/sentry/metrics/SentryMetricsParameters; public static fun create (Lio/sentry/SentryDate;Lio/sentry/SentryAttributes;)Lio/sentry/metrics/SentryMetricsParameters; public fun getAttributes ()Lio/sentry/SentryAttributes; + public fun getHint ()Lio/sentry/Hint; public fun getOrigin ()Ljava/lang/String; public fun getTimestamp ()Lio/sentry/SentryDate; public fun setAttributes (Lio/sentry/SentryAttributes;)V + public fun setHint (Lio/sentry/Hint;)V public fun setOrigin (Ljava/lang/String;)V public fun setTimestamp (Lio/sentry/SentryDate;)V } diff --git a/sentry/src/main/java/io/sentry/EventProcessor.java b/sentry/src/main/java/io/sentry/EventProcessor.java index 4f59de7578..928fd02209 100644 --- a/sentry/src/main/java/io/sentry/EventProcessor.java +++ b/sentry/src/main/java/io/sentry/EventProcessor.java @@ -63,7 +63,7 @@ default SentryLogEvent process(@NotNull SentryLogEvent event) { * @return the event itself, a mutated SentryMetricsEvent or null */ @Nullable - default SentryMetricsEvent process(@NotNull SentryMetricsEvent event) { + default SentryMetricsEvent process(@NotNull SentryMetricsEvent event, @NotNull Hint hint) { return event; } diff --git a/sentry/src/main/java/io/sentry/ISentryClient.java b/sentry/src/main/java/io/sentry/ISentryClient.java index 79172be060..98b6034bb7 100644 --- a/sentry/src/main/java/io/sentry/ISentryClient.java +++ b/sentry/src/main/java/io/sentry/ISentryClient.java @@ -305,7 +305,8 @@ SentryId captureProfileChunk( void captureLog(@NotNull SentryLogEvent logEvent, @Nullable IScope scope); - void captureMetric(@NotNull SentryMetricsEvent logEvent, @Nullable IScope scope); + void captureMetric( + @NotNull SentryMetricsEvent logEvent, @Nullable IScope scope, @Nullable Hint hint); @ApiStatus.Internal void captureBatchedLogEvents(@NotNull SentryLogEvents logEvents); diff --git a/sentry/src/main/java/io/sentry/NoOpSentryClient.java b/sentry/src/main/java/io/sentry/NoOpSentryClient.java index 47b3343874..961ef9031b 100644 --- a/sentry/src/main/java/io/sentry/NoOpSentryClient.java +++ b/sentry/src/main/java/io/sentry/NoOpSentryClient.java @@ -89,7 +89,8 @@ public void captureLog(@NotNull SentryLogEvent logEvent, @Nullable IScope scope) } @Override - public void captureMetric(@NotNull SentryMetricsEvent metricsEvent, @Nullable IScope scope) { + public void captureMetric( + @NotNull SentryMetricsEvent metricsEvent, @Nullable IScope scope, @Nullable Hint hint) { // do nothing } diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index f198de5342..26c70f365f 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -517,10 +517,12 @@ private SentryLogEvent processLogEvent( @Nullable private SentryMetricsEvent processMetricsEvent( - @NotNull SentryMetricsEvent event, final @NotNull List eventProcessors) { + @NotNull SentryMetricsEvent event, + final @NotNull List eventProcessors, + final @NotNull Hint hint) { for (final EventProcessor processor : eventProcessors) { try { - event = processor.process(event); + event = processor.process(event, hint); } catch (Throwable e) { options .getLogger() @@ -1278,23 +1280,30 @@ public void captureBatchedLogEvents(final @NotNull SentryLogEvents logEvents) { @ApiStatus.Experimental @Override - public void captureMetric(@Nullable SentryMetricsEvent metricsEvent, @Nullable IScope scope) { + public void captureMetric( + @Nullable SentryMetricsEvent metricsEvent, + final @Nullable IScope scope, + @Nullable Hint hint) { + if (hint == null) { + hint = new Hint(); + } + if (metricsEvent != null && scope != null) { - metricsEvent = processMetricsEvent(metricsEvent, scope.getEventProcessors()); + metricsEvent = processMetricsEvent(metricsEvent, scope.getEventProcessors(), hint); if (metricsEvent == null) { return; } } if (metricsEvent != null) { - metricsEvent = processMetricsEvent(metricsEvent, options.getEventProcessors()); + metricsEvent = processMetricsEvent(metricsEvent, options.getEventProcessors(), hint); if (metricsEvent == null) { return; } } if (metricsEvent != null) { - metricsEvent = executeBeforeSendMetric(metricsEvent); + metricsEvent = executeBeforeSendMetric(metricsEvent, hint); if (metricsEvent == null) { options @@ -1625,12 +1634,13 @@ private void sortBreadcrumbsByDate( return event; } - private @Nullable SentryMetricsEvent executeBeforeSendMetric(@NotNull SentryMetricsEvent event) { + private @Nullable SentryMetricsEvent executeBeforeSendMetric( + @NotNull SentryMetricsEvent event, final @NotNull Hint hint) { final SentryOptions.Metrics.BeforeSendMetricCallback beforeSendMetric = options.getMetrics().getBeforeSend(); if (beforeSendMetric != null) { try { - event = beforeSendMetric.execute(event); + event = beforeSendMetric.execute(event, hint); } catch (Throwable e) { options .getLogger() diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index eda92d09e9..f24f649092 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -3813,7 +3813,8 @@ public interface BeforeSendMetricCallback { * @return the original metric, mutated metric or null if metric was dropped */ @Nullable - SentryMetricsEvent execute(final @NotNull SentryMetricsEvent metric); + SentryMetricsEvent execute( + final @NotNull SentryMetricsEvent metric, final @NotNull Hint hint); } } diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java index 2afbccb3f9..9dc28dd5b1 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java @@ -158,7 +158,7 @@ private void captureMetrics( metricsEvent.setUnit(unit); metricsEvent.setAttributes(createAttributes(params)); - scopes.getClient().captureMetric(metricsEvent, combinedScope); + scopes.getClient().captureMetric(metricsEvent, combinedScope, params.getHint()); } catch (Throwable e) { options.getLogger().log(SentryLevel.ERROR, "Error while capturing log event", e); } diff --git a/sentry/src/main/java/io/sentry/metrics/SentryMetricsParameters.java b/sentry/src/main/java/io/sentry/metrics/SentryMetricsParameters.java index 14d8403c20..c651a456af 100644 --- a/sentry/src/main/java/io/sentry/metrics/SentryMetricsParameters.java +++ b/sentry/src/main/java/io/sentry/metrics/SentryMetricsParameters.java @@ -1,5 +1,6 @@ package io.sentry.metrics; +import io.sentry.Hint; import io.sentry.SentryAttributes; import io.sentry.SentryDate; import org.jetbrains.annotations.NotNull; @@ -11,6 +12,8 @@ public final class SentryMetricsParameters { private @Nullable SentryAttributes attributes; private @NotNull String origin = "manual"; + private @Nullable Hint hint = null; + public @Nullable SentryDate getTimestamp() { return timestamp; } @@ -35,6 +38,14 @@ public void setOrigin(final @NotNull String origin) { this.origin = origin; } + public @Nullable Hint getHint() { + return hint; + } + + public void setHint(final @Nullable Hint hint) { + this.hint = hint; + } + public static @NotNull SentryMetricsParameters create( final @Nullable SentryDate timestamp, final @Nullable SentryAttributes attributes) { final @NotNull SentryMetricsParameters params = new SentryMetricsParameters(); diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index fc17a30094..842b9d9799 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -3134,7 +3134,7 @@ class ScopesTest { sut.close() sut.metrics().count("metric name") - verify(mockClient, never()).captureMetric(any(), anyOrNull()) + verify(mockClient, never()).captureMetric(any(), anyOrNull(), anyOrNull()) } @Test @@ -3142,7 +3142,7 @@ class ScopesTest { val (sut, mockClient) = getEnabledScopes { it.metrics.isEnabled = false } sut.metrics().count("metric name") - verify(mockClient, never()).captureMetric(any(), anyOrNull()) + verify(mockClient, never()).captureMetric(any(), anyOrNull(), anyOrNull()) } @Test @@ -3159,6 +3159,7 @@ class ScopesTest { assertEquals("counter", it.type) }, anyOrNull(), + anyOrNull(), ) } @@ -3176,6 +3177,7 @@ class ScopesTest { assertEquals("gauge", it.type) }, anyOrNull(), + anyOrNull(), ) } @@ -3193,6 +3195,7 @@ class ScopesTest { assertEquals("distribution", it.type) }, anyOrNull(), + anyOrNull(), ) } @@ -3209,6 +3212,7 @@ class ScopesTest { assertNull(it.attributes!!.get("sentry.origin")) }, anyOrNull(), + anyOrNull(), ) } @@ -3230,6 +3234,7 @@ class ScopesTest { ) }, anyOrNull(), + anyOrNull(), ) } @@ -3248,6 +3253,7 @@ class ScopesTest { assertEquals("counter", it.type) }, anyOrNull(), + anyOrNull(), ) } @@ -3265,6 +3271,7 @@ class ScopesTest { assertEquals("counter", it.type) }, anyOrNull(), + anyOrNull(), ) } @@ -3283,6 +3290,7 @@ class ScopesTest { assertEquals("counter", it.type) }, anyOrNull(), + anyOrNull(), ) } @@ -3312,6 +3320,7 @@ class ScopesTest { assertEquals("string", attr1.type) }, anyOrNull(), + anyOrNull(), ) } @@ -3380,6 +3389,7 @@ class ScopesTest { assertEquals("double", nameddoubleattr.type) }, anyOrNull(), + anyOrNull(), ) } @@ -3412,6 +3422,7 @@ class ScopesTest { assertEquals("string", attr1.type) }, anyOrNull(), + anyOrNull(), ) } @@ -3430,6 +3441,7 @@ class ScopesTest { assertEquals("distribution", it.type) }, anyOrNull(), + anyOrNull(), ) } @@ -3447,6 +3459,7 @@ class ScopesTest { assertEquals("distribution", it.type) }, anyOrNull(), + anyOrNull(), ) } @@ -3476,6 +3489,7 @@ class ScopesTest { assertEquals("string", attr1.type) }, anyOrNull(), + anyOrNull(), ) } @@ -3544,6 +3558,7 @@ class ScopesTest { assertEquals("double", nameddoubleattr.type) }, anyOrNull(), + anyOrNull(), ) } @@ -3576,6 +3591,7 @@ class ScopesTest { assertEquals("string", attr1.type) }, anyOrNull(), + anyOrNull(), ) } @@ -3594,6 +3610,7 @@ class ScopesTest { assertEquals("gauge", it.type) }, anyOrNull(), + anyOrNull(), ) } @@ -3611,6 +3628,7 @@ class ScopesTest { assertEquals("gauge", it.type) }, anyOrNull(), + anyOrNull(), ) } @@ -3640,6 +3658,7 @@ class ScopesTest { assertEquals("string", attr1.type) }, anyOrNull(), + anyOrNull(), ) } @@ -3708,6 +3727,7 @@ class ScopesTest { assertEquals("double", nameddoubleattr.type) }, anyOrNull(), + anyOrNull(), ) } @@ -3740,6 +3760,7 @@ class ScopesTest { assertEquals("string", attr1.type) }, anyOrNull(), + anyOrNull(), ) } @@ -3779,6 +3800,7 @@ class ScopesTest { assertEquals("string", userEmail.type) }, anyOrNull(), + anyOrNull(), ) } @@ -3806,6 +3828,7 @@ class ScopesTest { assertNull(it.attributes?.get("user.email")) }, anyOrNull(), + anyOrNull(), ) } @@ -3829,6 +3852,7 @@ class ScopesTest { assertNull(it.attributes?.get("user.email")) }, anyOrNull(), + anyOrNull(), ) } @@ -3851,6 +3875,7 @@ class ScopesTest { assertNull(it.attributes?.get("user.email")) }, anyOrNull(), + anyOrNull(), ) } @@ -3875,6 +3900,7 @@ class ScopesTest { assertNull(it.attributes?.get("user.email")) }, anyOrNull(), + anyOrNull(), ) } @@ -3893,6 +3919,7 @@ class ScopesTest { assertEquals(replayId.toString(), logReplayId.value) }, anyOrNull(), + anyOrNull(), ) } @@ -3909,6 +3936,7 @@ class ScopesTest { assertNull(logReplayId) }, anyOrNull(), + anyOrNull(), ) } @@ -3929,6 +3957,7 @@ class ScopesTest { assertNull(logReplayType) }, anyOrNull(), + anyOrNull(), ) } @@ -3950,6 +3979,7 @@ class ScopesTest { assertNull(logReplayType) }, anyOrNull(), + anyOrNull(), ) } @@ -3973,6 +4003,7 @@ class ScopesTest { assertTrue(logReplayType.value as Boolean) }, anyOrNull(), + anyOrNull(), ) } diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index ef7c8e7b08..e7c4ae84b9 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -361,7 +361,7 @@ class SentryClientTest { fun `when beforeSendMetric is set, callback is invoked`() { val scope = createScope() var invoked = false - fixture.sentryOptions.metrics.setBeforeSend { m -> + fixture.sentryOptions.metrics.setBeforeSend { m, hint -> invoked = true m } @@ -369,6 +369,7 @@ class SentryClientTest { sut.captureMetric( SentryMetricsEvent(SentryId(), SentryNanotimeDate(), "name", "gauge", 123.0), scope, + null, ) assertTrue(invoked) } @@ -376,11 +377,12 @@ class SentryClientTest { @Test fun `when beforeSendMetric returns null, metric is dropped`() { val scope = createScope() - fixture.sentryOptions.metrics.setBeforeSend { _: SentryMetricsEvent -> null } + fixture.sentryOptions.metrics.setBeforeSend { _: SentryMetricsEvent, hint -> null } val sut = fixture.getSut() sut.captureMetric( SentryMetricsEvent(SentryId(), SentryNanotimeDate(), "name", "gauge", 123.0), scope, + null, ) verify(fixture.transport, never()).send(any(), anyOrNull()) @@ -396,11 +398,12 @@ class SentryClientTest { val exception = Exception("test") exception.stackTrace.toString() - fixture.sentryOptions.metrics.setBeforeSend { _ -> throw exception } + fixture.sentryOptions.metrics.setBeforeSend { _, hint -> throw exception } val sut = fixture.getSut() sut.captureMetric( SentryMetricsEvent(SentryId(), SentryNanotimeDate(), "name", "gauge", 123.0), scope, + null, ) assertClientReport( @@ -414,13 +417,13 @@ class SentryClientTest { val scope = createScope() val expected = SentryMetricsEvent(SentryId(), SentryNanotimeDate(), "expected name", "gauge", 123.0) - fixture.sentryOptions.metrics.setBeforeSend { _ -> expected } + fixture.sentryOptions.metrics.setBeforeSend { _, hint -> expected } val sut = fixture.getSut() val batchProcessor = mock() sut.injectForField("metricsBatchProcessor", batchProcessor) val actual = SentryMetricsEvent(SentryId(), SentryNanotimeDate(), "actual name", "counter", 97.0) - sut.captureMetric(actual, scope) + sut.captureMetric(actual, scope, null) verify(batchProcessor).add(check { assertEquals("expected name", it.name) }) verifyNoMoreInteractions(batchProcessor) } From 5bfb646f52c0c48f2100258a597a990796a00092 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 12 Jan 2026 10:30:39 +0100 Subject: [PATCH 30/40] SentryMetricsParameters create shortcut for attributes map --- sentry/api/sentry.api | 1 + .../metrics/SentryMetricsParameters.java | 12 ++++++++ sentry/src/test/java/io/sentry/ScopesTest.kt | 30 +++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 1f0783721e..1970c3fefb 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -5283,6 +5283,7 @@ public final class io/sentry/metrics/SentryMetricsParameters { public fun ()V public static fun create (Lio/sentry/SentryAttributes;)Lio/sentry/metrics/SentryMetricsParameters; public static fun create (Lio/sentry/SentryDate;Lio/sentry/SentryAttributes;)Lio/sentry/metrics/SentryMetricsParameters; + public static fun create (Ljava/util/Map;)Lio/sentry/metrics/SentryMetricsParameters; public fun getAttributes ()Lio/sentry/SentryAttributes; public fun getHint ()Lio/sentry/Hint; public fun getOrigin ()Ljava/lang/String; diff --git a/sentry/src/main/java/io/sentry/metrics/SentryMetricsParameters.java b/sentry/src/main/java/io/sentry/metrics/SentryMetricsParameters.java index c651a456af..b52a749497 100644 --- a/sentry/src/main/java/io/sentry/metrics/SentryMetricsParameters.java +++ b/sentry/src/main/java/io/sentry/metrics/SentryMetricsParameters.java @@ -3,6 +3,7 @@ import io.sentry.Hint; import io.sentry.SentryAttributes; import io.sentry.SentryDate; +import java.util.Map; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -60,4 +61,15 @@ public void setHint(final @Nullable Hint hint) { final @Nullable SentryAttributes attributes) { return create(null, attributes); } + + /** + * A shortcut for SentryMetricsParameters.create(SentryAttributes.fromMap()) + * + * @param attributes a map of attributes + * @return parameters + */ + public static @NotNull SentryMetricsParameters create( + final @Nullable Map attributes) { + return create(null, SentryAttributes.fromMap(attributes)); + } } diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index 842b9d9799..1e22be9877 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -3324,6 +3324,36 @@ class ScopesTest { ) } + @Test + fun `creating count metric with attributes from map and shortcut factory method works`() { + val (sut, mockClient) = getEnabledScopes() + + sut + .metrics() + .count( + "metric name", + 1.0, + "visit", + SentryMetricsParameters.create(mapOf("attrname1" to "attrval1")), + ) + + verify(mockClient) + .captureMetric( + check { + assertEquals("metric name", it.name) + assertEquals(1.0, it.value) + assertEquals("visit", it.unit) + assertEquals("counter", it.type) + + val attr1 = it.attributes?.get("attrname1")!! + assertEquals("attrval1", attr1.value) + assertEquals("string", attr1.type) + }, + anyOrNull(), + anyOrNull(), + ) + } + @Test fun `creating count metric with attributes works`() { val (sut, mockClient) = getEnabledScopes() From 4d164529d950668fd41b0164b3101ccd57096c9e Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 12 Jan 2026 10:51:41 +0100 Subject: [PATCH 31/40] Automatically use SentryOptions.Metrics.BeforeSendMetricCallback Spring beans --- .../samples/spring7/web/MetricController.java | 4 ++-- .../io/sentry/systemtest/MetricsSystemTest.kt | 2 +- .../spring/jakarta/web/MetricController.java | 4 ++-- .../io/sentry/systemtest/MetricsSystemTest.kt | 2 +- .../samples/spring/web/MetricController.java | 4 ++-- .../io/sentry/systemtest/MetricsSystemTest.kt | 2 +- .../spring/boot4/SentryAutoConfiguration.java | 4 ++++ .../boot4/SentryAutoConfigurationTest.kt | 22 +++++++++++++++++++ .../boot/jakarta/SentryAutoConfiguration.java | 4 ++++ .../jakarta/SentryAutoConfigurationTest.kt | 22 +++++++++++++++++++ .../spring/boot/SentryAutoConfiguration.java | 4 ++++ .../boot/SentryAutoConfigurationTest.kt | 22 +++++++++++++++++++ 12 files changed, 87 insertions(+), 9 deletions(-) diff --git a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/MetricController.java b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/MetricController.java index cc8386fde2..d29fc1a89a 100644 --- a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/MetricController.java +++ b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/MetricController.java @@ -20,13 +20,13 @@ String count() { } @GetMapping("gauge/{count}") - String gauge(@PathVariable Long count) { + String gauge(@PathVariable("count") Long count) { Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); return "gauge metric tracked"; } @GetMapping("distribution/{count}") - String distribution(@PathVariable Long count) { + String distribution(@PathVariable("count") Long count) { Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); return "distribution metric tracked"; } diff --git a/sentry-samples/sentry-samples-spring-7/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-7/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt index a379681adb..9c9bb8baa8 100644 --- a/sentry-samples/sentry-samples-spring-7/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-7/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -10,7 +10,7 @@ class MetricsSystemTest { @Before fun setup() { - testHelper = TestHelper("http://localhost:8080") + testHelper = TestHelper("http://localhost:8080/sentry-samples-spring-7-0.0.1-SNAPSHOT") testHelper.reset() } diff --git a/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/MetricController.java b/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/MetricController.java index f2e609b0f8..fb1526fb8a 100644 --- a/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/MetricController.java +++ b/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/MetricController.java @@ -20,13 +20,13 @@ String count() { } @GetMapping("gauge/{count}") - String gauge(@PathVariable Long count) { + String gauge(@PathVariable("count") Long count) { Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); return "gauge metric tracked"; } @GetMapping("distribution/{count}") - String distribution(@PathVariable Long count) { + String distribution(@PathVariable("count") Long count) { Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); return "distribution metric tracked"; } diff --git a/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt index a379681adb..6450307e22 100644 --- a/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -10,7 +10,7 @@ class MetricsSystemTest { @Before fun setup() { - testHelper = TestHelper("http://localhost:8080") + testHelper = TestHelper("http://localhost:8080/sentry-samples-spring-jakarta-0.0.1-SNAPSHOT") testHelper.reset() } diff --git a/sentry-samples/sentry-samples-spring/src/main/java/io/sentry/samples/spring/web/MetricController.java b/sentry-samples/sentry-samples-spring/src/main/java/io/sentry/samples/spring/web/MetricController.java index b425f973a6..82ca41b73c 100644 --- a/sentry-samples/sentry-samples-spring/src/main/java/io/sentry/samples/spring/web/MetricController.java +++ b/sentry-samples/sentry-samples-spring/src/main/java/io/sentry/samples/spring/web/MetricController.java @@ -20,13 +20,13 @@ String count() { } @GetMapping("gauge/{count}") - String gauge(@PathVariable Long count) { + String gauge(@PathVariable("count") Long count) { Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); return "gauge metric tracked"; } @GetMapping("distribution/{count}") - String distribution(@PathVariable Long count) { + String distribution(@PathVariable("count") Long count) { Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); return "distribution metric tracked"; } diff --git a/sentry-samples/sentry-samples-spring/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt index a379681adb..3a4b586b4f 100644 --- a/sentry-samples/sentry-samples-spring/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt +++ b/sentry-samples/sentry-samples-spring/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -10,7 +10,7 @@ class MetricsSystemTest { @Before fun setup() { - testHelper = TestHelper("http://localhost:8080") + testHelper = TestHelper("http://localhost:8080/sentry-samples-spring-0.0.1-SNAPSHOT") testHelper.reset() } diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryAutoConfiguration.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryAutoConfiguration.java index 618895e51f..1b804e8cb8 100644 --- a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryAutoConfiguration.java +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryAutoConfiguration.java @@ -105,6 +105,8 @@ static class HubConfiguration { beforeSendTransactionCallback, final @NotNull ObjectProvider beforeSendLogsCallback, + final @NotNull ObjectProvider + beforeSendMetricCallback, final @NotNull ObjectProvider beforeBreadcrumbCallback, final @NotNull ObjectProvider tracesSamplerCallback, @@ -117,6 +119,8 @@ static class HubConfiguration { beforeSendCallback.ifAvailable(options::setBeforeSend); beforeSendTransactionCallback.ifAvailable(options::setBeforeSendTransaction); beforeSendLogsCallback.ifAvailable(callback -> options.getLogs().setBeforeSend(callback)); + beforeSendMetricCallback.ifAvailable( + callback -> options.getMetrics().setBeforeSend(callback)); beforeBreadcrumbCallback.ifAvailable(options::setBeforeBreadcrumb); tracesSamplerCallback.ifAvailable(options::setTracesSampler); eventProcessors.forEach(options::addEventProcessor); diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt index 14a79a761f..51bbb8d83d 100644 --- a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt @@ -21,6 +21,7 @@ import io.sentry.SentryEvent import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryLevel import io.sentry.SentryLogEvent +import io.sentry.SentryMetricsEvent import io.sentry.SentryOptions import io.sentry.asyncprofiler.profiling.JavaContinuousProfiler import io.sentry.asyncprofiler.provider.AsyncProfilerProfileConverterProvider @@ -370,6 +371,17 @@ class SentryAutoConfigurationTest { } } + @Test + fun `registers metrics beforeSendCallback on SentryOptions`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withUserConfiguration(CustomBeforeSendMetricCallbackConfiguration::class.java) + .run { + assertThat(it.getBean(SentryOptions::class.java).metrics.beforeSend) + .isInstanceOf(CustomBeforeSendMetricCallback::class.java) + } + } + @Test fun `registers beforeBreadcrumbCallback on SentryOptions`() { contextRunner @@ -1240,6 +1252,16 @@ class SentryAutoConfigurationTest { override fun execute(event: SentryLogEvent): SentryLogEvent? = null } + @Configuration(proxyBeanMethods = false) + open class CustomBeforeSendMetricCallbackConfiguration { + + @Bean open fun beforeSendCallback() = CustomBeforeSendMetricCallback() + } + + class CustomBeforeSendMetricCallback : SentryOptions.Metrics.BeforeSendMetricCallback { + override fun execute(metric: SentryMetricsEvent, hint: Hint): SentryMetricsEvent? = null + } + @Configuration(proxyBeanMethods = false) open class CustomBeforeSendTransactionCallbackConfiguration { diff --git a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java index 9ae80432ee..8663dac8c5 100644 --- a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java +++ b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java @@ -105,6 +105,8 @@ static class HubConfiguration { beforeSendTransactionCallback, final @NotNull ObjectProvider beforeSendLogsCallback, + final @NotNull ObjectProvider + beforeSendMetricCallback, final @NotNull ObjectProvider beforeBreadcrumbCallback, final @NotNull ObjectProvider onDiscardCallback, @@ -118,6 +120,8 @@ static class HubConfiguration { beforeSendCallback.ifAvailable(options::setBeforeSend); beforeSendTransactionCallback.ifAvailable(options::setBeforeSendTransaction); beforeSendLogsCallback.ifAvailable(callback -> options.getLogs().setBeforeSend(callback)); + beforeSendMetricCallback.ifAvailable( + callback -> options.getMetrics().setBeforeSend(callback)); beforeBreadcrumbCallback.ifAvailable(options::setBeforeBreadcrumb); onDiscardCallback.ifAvailable(options::setOnDiscard); tracesSamplerCallback.ifAvailable(options::setTracesSampler); diff --git a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt index 7dadbf6024..72ed7b8d8b 100644 --- a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt @@ -23,6 +23,7 @@ import io.sentry.SentryEvent import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryLevel import io.sentry.SentryLogEvent +import io.sentry.SentryMetricsEvent import io.sentry.SentryOptions import io.sentry.asyncprofiler.profiling.JavaContinuousProfiler import io.sentry.asyncprofiler.provider.AsyncProfilerProfileConverterProvider @@ -381,6 +382,17 @@ class SentryAutoConfigurationTest { } } + @Test + fun `registers metrics beforeSendCallback on SentryOptions`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withUserConfiguration(CustomBeforeSendMetricCallbackConfiguration::class.java) + .run { + assertThat(it.getBean(SentryOptions::class.java).metrics.beforeSend) + .isInstanceOf(CustomBeforeSendMetricCallback::class.java) + } + } + @Test fun `registers beforeBreadcrumbCallback on SentryOptions`() { contextRunner @@ -1262,6 +1274,16 @@ class SentryAutoConfigurationTest { override fun execute(event: SentryLogEvent): SentryLogEvent? = null } + @Configuration(proxyBeanMethods = false) + open class CustomBeforeSendMetricCallbackConfiguration { + + @Bean open fun beforeSendCallback() = CustomBeforeSendMetricCallback() + } + + class CustomBeforeSendMetricCallback : SentryOptions.Metrics.BeforeSendMetricCallback { + override fun execute(metric: SentryMetricsEvent, hint: Hint): SentryMetricsEvent? = null + } + @Configuration(proxyBeanMethods = false) open class CustomBeforeSendTransactionCallbackConfiguration { diff --git a/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java index ec7998eaa8..76424b5c55 100644 --- a/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java +++ b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java @@ -103,6 +103,8 @@ static class HubConfiguration { beforeSendTransactionCallback, final @NotNull ObjectProvider beforeSendLogsCallback, + final @NotNull ObjectProvider + beforeSendMetricCallback, final @NotNull ObjectProvider beforeBreadcrumbCallback, final @NotNull ObjectProvider onDiscardCallback, @@ -116,6 +118,8 @@ static class HubConfiguration { beforeSendCallback.ifAvailable(options::setBeforeSend); beforeSendTransactionCallback.ifAvailable(options::setBeforeSendTransaction); beforeSendLogsCallback.ifAvailable(callback -> options.getLogs().setBeforeSend(callback)); + beforeSendMetricCallback.ifAvailable( + callback -> options.getMetrics().setBeforeSend(callback)); beforeBreadcrumbCallback.ifAvailable(options::setBeforeBreadcrumb); onDiscardCallback.ifAvailable(options::setOnDiscard); tracesSamplerCallback.ifAvailable(options::setTracesSampler); diff --git a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt index 00cd7f06b1..70cb61d088 100644 --- a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt @@ -23,6 +23,7 @@ import io.sentry.SentryEvent import io.sentry.SentryIntegrationPackageStorage import io.sentry.SentryLevel import io.sentry.SentryLogEvent +import io.sentry.SentryMetricsEvent import io.sentry.SentryOptions import io.sentry.asyncprofiler.profiling.JavaContinuousProfiler import io.sentry.asyncprofiler.provider.AsyncProfilerProfileConverterProvider @@ -369,6 +370,17 @@ class SentryAutoConfigurationTest { } } + @Test + fun `registers metrics beforeSendCallback on SentryOptions`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withUserConfiguration(CustomBeforeSendMetricCallbackConfiguration::class.java) + .run { + assertThat(it.getBean(SentryOptions::class.java).metrics.beforeSend) + .isInstanceOf(CustomBeforeSendMetricCallback::class.java) + } + } + @Test fun `registers beforeSendTransactionCallback on SentryOptions`() { contextRunner @@ -1188,6 +1200,16 @@ class SentryAutoConfigurationTest { override fun execute(event: SentryLogEvent): SentryLogEvent? = null } + @Configuration(proxyBeanMethods = false) + open class CustomBeforeSendMetricCallbackConfiguration { + + @Bean open fun beforeSendCallback() = CustomBeforeSendMetricCallback() + } + + class CustomBeforeSendMetricCallback : SentryOptions.Metrics.BeforeSendMetricCallback { + override fun execute(metric: SentryMetricsEvent, hint: Hint): SentryMetricsEvent? = null + } + @Configuration(proxyBeanMethods = false) open class CustomBeforeSendTransactionCallbackConfiguration { From 6913b954cb15778b102e1d5635f6bb41032cb284 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 13 Jan 2026 12:53:14 +0100 Subject: [PATCH 32/40] Rename sample API param, remove sleep --- .../sentry/samples/spring7/web/MetricController.java | 12 ++++++------ .../kotlin/io/sentry/systemtest/MetricsSystemTest.kt | 6 ------ .../samples/spring/boot4/MetricController.java | 12 ++++++------ .../kotlin/io/sentry/systemtest/MetricsSystemTest.kt | 6 ------ .../samples/spring/boot4/MetricController.java | 12 ++++++------ .../kotlin/io/sentry/systemtest/MetricsSystemTest.kt | 6 ------ .../samples/spring/boot4/MetricController.java | 12 ++++++------ .../kotlin/io/sentry/systemtest/MetricsSystemTest.kt | 6 ------ .../samples/spring/boot4/MetricController.java | 12 ++++++------ .../kotlin/io/sentry/systemtest/MetricsSystemTest.kt | 6 ------ .../spring/boot/jakarta/MetricController.java | 12 ++++++------ .../kotlin/io/sentry/systemtest/MetricsSystemTest.kt | 6 ------ .../spring/boot/jakarta/MetricController.java | 12 ++++++------ .../kotlin/io/sentry/systemtest/MetricsSystemTest.kt | 6 ------ .../spring/boot/jakarta/MetricController.java | 12 ++++++------ .../kotlin/io/sentry/systemtest/MetricsSystemTest.kt | 6 ------ .../sentry/samples/spring/boot/MetricController.java | 12 ++++++------ .../kotlin/io/sentry/systemtest/MetricsSystemTest.kt | 6 ------ .../sentry/samples/spring/boot/MetricController.java | 12 ++++++------ .../kotlin/io/sentry/systemtest/MetricsSystemTest.kt | 6 ------ .../spring/boot/jakarta/MetricController.java | 12 ++++++------ .../kotlin/io/sentry/systemtest/MetricsSystemTest.kt | 6 ------ .../sentry/samples/spring/boot/MetricController.java | 12 ++++++------ .../kotlin/io/sentry/systemtest/MetricsSystemTest.kt | 6 ------ .../sentry/samples/spring/boot/MetricController.java | 12 ++++++------ .../kotlin/io/sentry/systemtest/MetricsSystemTest.kt | 6 ------ .../samples/spring/jakarta/web/MetricController.java | 12 ++++++------ .../kotlin/io/sentry/systemtest/MetricsSystemTest.kt | 6 ------ .../sentry/samples/spring/web/MetricController.java | 12 ++++++------ .../kotlin/io/sentry/systemtest/MetricsSystemTest.kt | 6 ------ .../io/sentry/systemtest/util/RestTestClient.kt | 8 ++++---- 31 files changed, 94 insertions(+), 184 deletions(-) diff --git a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/MetricController.java b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/MetricController.java index d29fc1a89a..7ea4d6d03b 100644 --- a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/MetricController.java +++ b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/MetricController.java @@ -19,15 +19,15 @@ String count() { return "count metric increased"; } - @GetMapping("gauge/{count}") - String gauge(@PathVariable("count") Long count) { - Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + @GetMapping("gauge/{value}") + String gauge(@PathVariable("value") Long value) { + Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); return "gauge metric tracked"; } - @GetMapping("distribution/{count}") - String distribution(@PathVariable("count") Long count) { - Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + @GetMapping("distribution/{value}") + String distribution(@PathVariable("value") Long value) { + Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); return "distribution metric tracked"; } } diff --git a/sentry-samples/sentry-samples-spring-7/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-7/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt index 9c9bb8baa8..3d7afa4f73 100644 --- a/sentry-samples/sentry-samples-spring-7/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-7/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -20,8 +20,6 @@ class MetricsSystemTest { assertEquals("count metric increased", restClient.getCountMetric()) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) } @@ -33,8 +31,6 @@ class MetricsSystemTest { assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) } @@ -46,8 +42,6 @@ class MetricsSystemTest { assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) } 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 f9163349a9..e6d014f983 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 @@ -19,15 +19,15 @@ String count() { return "count metric increased"; } - @GetMapping("gauge/{count}") - String gauge(@PathVariable Long count) { - Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + @GetMapping("gauge/{value}") + String gauge(@PathVariable Long value) { + Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); return "gauge metric tracked"; } - @GetMapping("distribution/{count}") - String distribution(@PathVariable Long count) { - Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + @GetMapping("distribution/{value}") + String distribution(@PathVariable Long value) { + Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); return "distribution metric tracked"; } } 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 a379681adb..dc2ca2a10a 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 @@ -20,8 +20,6 @@ class MetricsSystemTest { assertEquals("count metric increased", restClient.getCountMetric()) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) } @@ -33,8 +31,6 @@ class MetricsSystemTest { assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) } @@ -46,8 +42,6 @@ class MetricsSystemTest { assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) } 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 f9163349a9..e6d014f983 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 @@ -19,15 +19,15 @@ String count() { return "count metric increased"; } - @GetMapping("gauge/{count}") - String gauge(@PathVariable Long count) { - Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + @GetMapping("gauge/{value}") + String gauge(@PathVariable Long value) { + Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); return "gauge metric tracked"; } - @GetMapping("distribution/{count}") - String distribution(@PathVariable Long count) { - Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + @GetMapping("distribution/{value}") + String distribution(@PathVariable Long value) { + Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); return "distribution metric tracked"; } } 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 a379681adb..dc2ca2a10a 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 @@ -20,8 +20,6 @@ class MetricsSystemTest { assertEquals("count metric increased", restClient.getCountMetric()) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) } @@ -33,8 +31,6 @@ class MetricsSystemTest { assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) } @@ -46,8 +42,6 @@ class MetricsSystemTest { assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) } 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 f9163349a9..e6d014f983 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 @@ -19,15 +19,15 @@ String count() { return "count metric increased"; } - @GetMapping("gauge/{count}") - String gauge(@PathVariable Long count) { - Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + @GetMapping("gauge/{value}") + String gauge(@PathVariable Long value) { + Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); return "gauge metric tracked"; } - @GetMapping("distribution/{count}") - String distribution(@PathVariable Long count) { - Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + @GetMapping("distribution/{value}") + String distribution(@PathVariable Long value) { + Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); return "distribution metric tracked"; } } 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 a379681adb..dc2ca2a10a 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 @@ -20,8 +20,6 @@ class MetricsSystemTest { assertEquals("count metric increased", restClient.getCountMetric()) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) } @@ -33,8 +31,6 @@ class MetricsSystemTest { assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) } @@ -46,8 +42,6 @@ class MetricsSystemTest { assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) } 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 f9163349a9..e6d014f983 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 @@ -19,15 +19,15 @@ String count() { return "count metric increased"; } - @GetMapping("gauge/{count}") - String gauge(@PathVariable Long count) { - Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + @GetMapping("gauge/{value}") + String gauge(@PathVariable Long value) { + Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); return "gauge metric tracked"; } - @GetMapping("distribution/{count}") - String distribution(@PathVariable Long count) { - Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + @GetMapping("distribution/{value}") + String distribution(@PathVariable Long value) { + Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); return "distribution metric tracked"; } } 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 a379681adb..dc2ca2a10a 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 @@ -20,8 +20,6 @@ class MetricsSystemTest { assertEquals("count metric increased", restClient.getCountMetric()) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) } @@ -33,8 +31,6 @@ class MetricsSystemTest { assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) } @@ -46,8 +42,6 @@ class MetricsSystemTest { assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) } diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java index 8c6203c09e..5d8a0bf88d 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java @@ -19,15 +19,15 @@ String count() { return "count metric increased"; } - @GetMapping("gauge/{count}") - String gauge(@PathVariable Long count) { - Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + @GetMapping("gauge/{value}") + String gauge(@PathVariable Long value) { + Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); return "gauge metric tracked"; } - @GetMapping("distribution/{count}") - String distribution(@PathVariable Long count) { - Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + @GetMapping("distribution/{value}") + String distribution(@PathVariable Long value) { + Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); return "distribution metric tracked"; } } diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt index a379681adb..dc2ca2a10a 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -20,8 +20,6 @@ class MetricsSystemTest { assertEquals("count metric increased", restClient.getCountMetric()) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) } @@ -33,8 +31,6 @@ class MetricsSystemTest { assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) } @@ -46,8 +42,6 @@ class MetricsSystemTest { assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) } diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java index 8c6203c09e..5d8a0bf88d 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java @@ -19,15 +19,15 @@ String count() { return "count metric increased"; } - @GetMapping("gauge/{count}") - String gauge(@PathVariable Long count) { - Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + @GetMapping("gauge/{value}") + String gauge(@PathVariable Long value) { + Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); return "gauge metric tracked"; } - @GetMapping("distribution/{count}") - String distribution(@PathVariable Long count) { - Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + @GetMapping("distribution/{value}") + String distribution(@PathVariable Long value) { + Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); return "distribution metric tracked"; } } diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt index a379681adb..dc2ca2a10a 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -20,8 +20,6 @@ class MetricsSystemTest { assertEquals("count metric increased", restClient.getCountMetric()) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) } @@ -33,8 +31,6 @@ class MetricsSystemTest { assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) } @@ -46,8 +42,6 @@ class MetricsSystemTest { assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) } diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java index 8c6203c09e..5d8a0bf88d 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java @@ -19,15 +19,15 @@ String count() { return "count metric increased"; } - @GetMapping("gauge/{count}") - String gauge(@PathVariable Long count) { - Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + @GetMapping("gauge/{value}") + String gauge(@PathVariable Long value) { + Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); return "gauge metric tracked"; } - @GetMapping("distribution/{count}") - String distribution(@PathVariable Long count) { - Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + @GetMapping("distribution/{value}") + String distribution(@PathVariable Long value) { + Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); return "distribution metric tracked"; } } diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt index a379681adb..dc2ca2a10a 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -20,8 +20,6 @@ class MetricsSystemTest { assertEquals("count metric increased", restClient.getCountMetric()) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) } @@ -33,8 +31,6 @@ class MetricsSystemTest { assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) } @@ -46,8 +42,6 @@ class MetricsSystemTest { assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) } diff --git a/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/MetricController.java b/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/MetricController.java index 2f1dabf784..5caafc0b26 100644 --- a/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/MetricController.java +++ b/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/MetricController.java @@ -19,15 +19,15 @@ String count() { return "count metric increased"; } - @GetMapping("gauge/{count}") - String gauge(@PathVariable Long count) { - Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + @GetMapping("gauge/{value}") + String gauge(@PathVariable Long value) { + Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); return "gauge metric tracked"; } - @GetMapping("distribution/{count}") - String distribution(@PathVariable Long count) { - Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + @GetMapping("distribution/{value}") + String distribution(@PathVariable Long value) { + Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); return "distribution metric tracked"; } } diff --git a/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt index a379681adb..dc2ca2a10a 100644 --- a/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -20,8 +20,6 @@ class MetricsSystemTest { assertEquals("count metric increased", restClient.getCountMetric()) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) } @@ -33,8 +31,6 @@ class MetricsSystemTest { assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) } @@ -46,8 +42,6 @@ class MetricsSystemTest { assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) } diff --git a/sentry-samples/sentry-samples-spring-boot-opentelemetry/src/main/java/io/sentry/samples/spring/boot/MetricController.java b/sentry-samples/sentry-samples-spring-boot-opentelemetry/src/main/java/io/sentry/samples/spring/boot/MetricController.java index 2f1dabf784..5caafc0b26 100644 --- a/sentry-samples/sentry-samples-spring-boot-opentelemetry/src/main/java/io/sentry/samples/spring/boot/MetricController.java +++ b/sentry-samples/sentry-samples-spring-boot-opentelemetry/src/main/java/io/sentry/samples/spring/boot/MetricController.java @@ -19,15 +19,15 @@ String count() { return "count metric increased"; } - @GetMapping("gauge/{count}") - String gauge(@PathVariable Long count) { - Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + @GetMapping("gauge/{value}") + String gauge(@PathVariable Long value) { + Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); return "gauge metric tracked"; } - @GetMapping("distribution/{count}") - String distribution(@PathVariable Long count) { - Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + @GetMapping("distribution/{value}") + String distribution(@PathVariable Long value) { + Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); return "distribution metric tracked"; } } diff --git a/sentry-samples/sentry-samples-spring-boot-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt index a379681adb..dc2ca2a10a 100644 --- a/sentry-samples/sentry-samples-spring-boot-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-opentelemetry/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -20,8 +20,6 @@ class MetricsSystemTest { assertEquals("count metric increased", restClient.getCountMetric()) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) } @@ -33,8 +31,6 @@ class MetricsSystemTest { assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) } @@ -46,8 +42,6 @@ class MetricsSystemTest { assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) } diff --git a/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java index 8c6203c09e..5d8a0bf88d 100644 --- a/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java +++ b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java @@ -19,15 +19,15 @@ String count() { return "count metric increased"; } - @GetMapping("gauge/{count}") - String gauge(@PathVariable Long count) { - Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + @GetMapping("gauge/{value}") + String gauge(@PathVariable Long value) { + Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); return "gauge metric tracked"; } - @GetMapping("distribution/{count}") - String distribution(@PathVariable Long count) { - Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + @GetMapping("distribution/{value}") + String distribution(@PathVariable Long value) { + Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); return "distribution metric tracked"; } } diff --git a/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt index a379681adb..dc2ca2a10a 100644 --- a/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -20,8 +20,6 @@ class MetricsSystemTest { assertEquals("count metric increased", restClient.getCountMetric()) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) } @@ -33,8 +31,6 @@ class MetricsSystemTest { assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) } @@ -46,8 +42,6 @@ class MetricsSystemTest { assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) } diff --git a/sentry-samples/sentry-samples-spring-boot-webflux/src/main/java/io/sentry/samples/spring/boot/MetricController.java b/sentry-samples/sentry-samples-spring-boot-webflux/src/main/java/io/sentry/samples/spring/boot/MetricController.java index 2f1dabf784..5caafc0b26 100644 --- a/sentry-samples/sentry-samples-spring-boot-webflux/src/main/java/io/sentry/samples/spring/boot/MetricController.java +++ b/sentry-samples/sentry-samples-spring-boot-webflux/src/main/java/io/sentry/samples/spring/boot/MetricController.java @@ -19,15 +19,15 @@ String count() { return "count metric increased"; } - @GetMapping("gauge/{count}") - String gauge(@PathVariable Long count) { - Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + @GetMapping("gauge/{value}") + String gauge(@PathVariable Long value) { + Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); return "gauge metric tracked"; } - @GetMapping("distribution/{count}") - String distribution(@PathVariable Long count) { - Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + @GetMapping("distribution/{value}") + String distribution(@PathVariable Long value) { + Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); return "distribution metric tracked"; } } diff --git a/sentry-samples/sentry-samples-spring-boot-webflux/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-webflux/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt index a379681adb..dc2ca2a10a 100644 --- a/sentry-samples/sentry-samples-spring-boot-webflux/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot-webflux/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -20,8 +20,6 @@ class MetricsSystemTest { assertEquals("count metric increased", restClient.getCountMetric()) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) } @@ -33,8 +31,6 @@ class MetricsSystemTest { assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) } @@ -46,8 +42,6 @@ class MetricsSystemTest { assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) } diff --git a/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/MetricController.java b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/MetricController.java index 2f1dabf784..5caafc0b26 100644 --- a/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/MetricController.java +++ b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/MetricController.java @@ -19,15 +19,15 @@ String count() { return "count metric increased"; } - @GetMapping("gauge/{count}") - String gauge(@PathVariable Long count) { - Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + @GetMapping("gauge/{value}") + String gauge(@PathVariable Long value) { + Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); return "gauge metric tracked"; } - @GetMapping("distribution/{count}") - String distribution(@PathVariable Long count) { - Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + @GetMapping("distribution/{value}") + String distribution(@PathVariable Long value) { + Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); return "distribution metric tracked"; } } diff --git a/sentry-samples/sentry-samples-spring-boot/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-boot/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt index a379681adb..dc2ca2a10a 100644 --- a/sentry-samples/sentry-samples-spring-boot/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-boot/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -20,8 +20,6 @@ class MetricsSystemTest { assertEquals("count metric increased", restClient.getCountMetric()) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) } @@ -33,8 +31,6 @@ class MetricsSystemTest { assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) } @@ -46,8 +42,6 @@ class MetricsSystemTest { assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) } diff --git a/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/MetricController.java b/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/MetricController.java index fb1526fb8a..cca33e1ede 100644 --- a/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/MetricController.java +++ b/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/MetricController.java @@ -19,15 +19,15 @@ String count() { return "count metric increased"; } - @GetMapping("gauge/{count}") - String gauge(@PathVariable("count") Long count) { - Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + @GetMapping("gauge/{value}") + String gauge(@PathVariable("value") Long value) { + Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); return "gauge metric tracked"; } - @GetMapping("distribution/{count}") - String distribution(@PathVariable("count") Long count) { - Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + @GetMapping("distribution/{value}") + String distribution(@PathVariable("value") Long value) { + Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); return "distribution metric tracked"; } } diff --git a/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt index 6450307e22..cef07e4866 100644 --- a/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt +++ b/sentry-samples/sentry-samples-spring-jakarta/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -20,8 +20,6 @@ class MetricsSystemTest { assertEquals("count metric increased", restClient.getCountMetric()) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) } @@ -33,8 +31,6 @@ class MetricsSystemTest { assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) } @@ -46,8 +42,6 @@ class MetricsSystemTest { assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) } diff --git a/sentry-samples/sentry-samples-spring/src/main/java/io/sentry/samples/spring/web/MetricController.java b/sentry-samples/sentry-samples-spring/src/main/java/io/sentry/samples/spring/web/MetricController.java index 82ca41b73c..a4359d5305 100644 --- a/sentry-samples/sentry-samples-spring/src/main/java/io/sentry/samples/spring/web/MetricController.java +++ b/sentry-samples/sentry-samples-spring/src/main/java/io/sentry/samples/spring/web/MetricController.java @@ -19,15 +19,15 @@ String count() { return "count metric increased"; } - @GetMapping("gauge/{count}") - String gauge(@PathVariable("count") Long count) { - Sentry.metrics().gauge("memory.free", count.doubleValue(), "byte"); + @GetMapping("gauge/{value}") + String gauge(@PathVariable("value") Long value) { + Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); return "gauge metric tracked"; } - @GetMapping("distribution/{count}") - String distribution(@PathVariable("count") Long count) { - Sentry.metrics().distribution("distributionMetric", count.doubleValue(), "child"); + @GetMapping("distribution/{value}") + String distribution(@PathVariable("value") Long value) { + Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); return "distribution metric tracked"; } } diff --git a/sentry-samples/sentry-samples-spring/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt b/sentry-samples/sentry-samples-spring/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt index 3a4b586b4f..6da3d98577 100644 --- a/sentry-samples/sentry-samples-spring/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt +++ b/sentry-samples/sentry-samples-spring/src/test/kotlin/io/sentry/systemtest/MetricsSystemTest.kt @@ -20,8 +20,6 @@ class MetricsSystemTest { assertEquals("count metric increased", restClient.getCountMetric()) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "countMetric", "counter", 1.0) } @@ -33,8 +31,6 @@ class MetricsSystemTest { assertEquals("gauge metric tracked", restClient.getGaugeMetric(14)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "memory.free", "gauge", 14.0) } @@ -46,8 +42,6 @@ class MetricsSystemTest { assertEquals("distribution metric tracked", restClient.getDistributionMetric(23)) assertEquals(200, restClient.lastKnownStatusCode) - Thread.sleep(10000) - testHelper.ensureMetricsReceived { event, header -> testHelper.doesContainMetric(event, "distributionMetric", "distribution", 23.0) } diff --git a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt index 695c853891..bdaa2333f2 100644 --- a/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt +++ b/sentry-system-test-support/src/main/kotlin/io/sentry/systemtest/util/RestTestClient.kt @@ -69,14 +69,14 @@ class RestTestClient(private val backendBaseUrl: String) : LoggingInsecureRestCl return callTyped(request, true) } - fun getGaugeMetric(count: Long): String? { - val request = Request.Builder().url("$backendBaseUrl/metric/gauge/$count") + fun getGaugeMetric(value: Long): String? { + val request = Request.Builder().url("$backendBaseUrl/metric/gauge/$value") return callTyped(request, true) } - fun getDistributionMetric(count: Long): String? { - val request = Request.Builder().url("$backendBaseUrl/metric/distribution/$count") + fun getDistributionMetric(value: Long): String? { + val request = Request.Builder().url("$backendBaseUrl/metric/distribution/$value") return callTyped(request, true) } From 81e4b6a3021f779d5b4330d2071da07b8502be58 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 13 Jan 2026 14:33:05 +0100 Subject: [PATCH 33/40] Create MetricsUnit constants --- .../samples/spring7/web/MetricController.java | 6 +- .../spring/boot4/MetricController.java | 10 +- .../spring/boot4/MetricController.java | 10 +- .../spring/boot4/MetricController.java | 10 +- .../spring/boot4/MetricController.java | 10 +- .../spring/boot/jakarta/MetricController.java | 10 +- .../spring/boot/jakarta/MetricController.java | 10 +- .../spring/boot/jakarta/MetricController.java | 10 +- .../samples/spring/boot/MetricController.java | 10 +- .../samples/spring/boot/MetricController.java | 10 +- .../spring/boot/jakarta/MetricController.java | 10 +- .../samples/spring/boot/MetricController.java | 10 +- .../samples/spring/boot/MetricController.java | 10 +- .../spring/jakarta/web/MetricController.java | 6 +- .../samples/spring/web/MetricController.java | 6 +- .../api/sentry-system-test-support.api | 3 +- .../io/sentry/systemtest/util/TestHelper.kt | 6 +- sentry/api/sentry.api | 36 +++++++ .../java/io/sentry/metrics/MetricsUnit.java | 98 +++++++++++++++++++ sentry/src/test/java/io/sentry/ScopesTest.kt | 59 +++++------ 20 files changed, 255 insertions(+), 85 deletions(-) create mode 100644 sentry/src/main/java/io/sentry/metrics/MetricsUnit.java diff --git a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/MetricController.java b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/MetricController.java index 7ea4d6d03b..b7f7d566b9 100644 --- a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/MetricController.java +++ b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/MetricController.java @@ -1,6 +1,7 @@ package io.sentry.samples.spring7.web; import io.sentry.Sentry; +import io.sentry.metrics.MetricsUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; @@ -21,13 +22,14 @@ String count() { @GetMapping("gauge/{value}") String gauge(@PathVariable("value") Long value) { - Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); + Sentry.metrics().gauge("memory.free", value.doubleValue(), MetricsUnit.Information.BYTE); return "gauge metric tracked"; } @GetMapping("distribution/{value}") String distribution(@PathVariable("value") Long value) { - Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); + Sentry.metrics() + .distribution("distributionMetric", value.doubleValue(), MetricsUnit.Duration.MILLISECOND); return "distribution metric tracked"; } } 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 e6d014f983..2a969ec884 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 @@ -1,6 +1,7 @@ package io.sentry.samples.spring.boot4; import io.sentry.Sentry; +import io.sentry.metrics.MetricsUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; @@ -20,14 +21,15 @@ String count() { } @GetMapping("gauge/{value}") - String gauge(@PathVariable Long value) { - Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); + String gauge(@PathVariable("value") Long value) { + Sentry.metrics().gauge("memory.free", value.doubleValue(), MetricsUnit.Information.BYTE); return "gauge metric tracked"; } @GetMapping("distribution/{value}") - String distribution(@PathVariable Long value) { - Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); + String distribution(@PathVariable("value") Long value) { + Sentry.metrics() + .distribution("distributionMetric", value.doubleValue(), MetricsUnit.Duration.MILLISECOND); return "distribution metric tracked"; } } 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 e6d014f983..2a969ec884 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 @@ -1,6 +1,7 @@ package io.sentry.samples.spring.boot4; import io.sentry.Sentry; +import io.sentry.metrics.MetricsUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; @@ -20,14 +21,15 @@ String count() { } @GetMapping("gauge/{value}") - String gauge(@PathVariable Long value) { - Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); + String gauge(@PathVariable("value") Long value) { + Sentry.metrics().gauge("memory.free", value.doubleValue(), MetricsUnit.Information.BYTE); return "gauge metric tracked"; } @GetMapping("distribution/{value}") - String distribution(@PathVariable Long value) { - Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); + String distribution(@PathVariable("value") Long value) { + Sentry.metrics() + .distribution("distributionMetric", value.doubleValue(), MetricsUnit.Duration.MILLISECOND); return "distribution metric tracked"; } } 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 e6d014f983..2a969ec884 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 @@ -1,6 +1,7 @@ package io.sentry.samples.spring.boot4; import io.sentry.Sentry; +import io.sentry.metrics.MetricsUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; @@ -20,14 +21,15 @@ String count() { } @GetMapping("gauge/{value}") - String gauge(@PathVariable Long value) { - Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); + String gauge(@PathVariable("value") Long value) { + Sentry.metrics().gauge("memory.free", value.doubleValue(), MetricsUnit.Information.BYTE); return "gauge metric tracked"; } @GetMapping("distribution/{value}") - String distribution(@PathVariable Long value) { - Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); + String distribution(@PathVariable("value") Long value) { + Sentry.metrics() + .distribution("distributionMetric", value.doubleValue(), MetricsUnit.Duration.MILLISECOND); return "distribution metric tracked"; } } 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 e6d014f983..2a969ec884 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 @@ -1,6 +1,7 @@ package io.sentry.samples.spring.boot4; import io.sentry.Sentry; +import io.sentry.metrics.MetricsUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; @@ -20,14 +21,15 @@ String count() { } @GetMapping("gauge/{value}") - String gauge(@PathVariable Long value) { - Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); + String gauge(@PathVariable("value") Long value) { + Sentry.metrics().gauge("memory.free", value.doubleValue(), MetricsUnit.Information.BYTE); return "gauge metric tracked"; } @GetMapping("distribution/{value}") - String distribution(@PathVariable Long value) { - Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); + String distribution(@PathVariable("value") Long value) { + Sentry.metrics() + .distribution("distributionMetric", value.doubleValue(), MetricsUnit.Duration.MILLISECOND); return "distribution metric tracked"; } } diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java index 5d8a0bf88d..f7c7529525 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java @@ -1,6 +1,7 @@ package io.sentry.samples.spring.boot.jakarta; import io.sentry.Sentry; +import io.sentry.metrics.MetricsUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; @@ -20,14 +21,15 @@ String count() { } @GetMapping("gauge/{value}") - String gauge(@PathVariable Long value) { - Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); + String gauge(@PathVariable("value") Long value) { + Sentry.metrics().gauge("memory.free", value.doubleValue(), MetricsUnit.Information.BYTE); return "gauge metric tracked"; } @GetMapping("distribution/{value}") - String distribution(@PathVariable Long value) { - Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); + String distribution(@PathVariable("value") Long value) { + Sentry.metrics() + .distribution("distributionMetric", value.doubleValue(), MetricsUnit.Duration.MILLISECOND); return "distribution metric tracked"; } } diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java index 5d8a0bf88d..f7c7529525 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java @@ -1,6 +1,7 @@ package io.sentry.samples.spring.boot.jakarta; import io.sentry.Sentry; +import io.sentry.metrics.MetricsUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; @@ -20,14 +21,15 @@ String count() { } @GetMapping("gauge/{value}") - String gauge(@PathVariable Long value) { - Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); + String gauge(@PathVariable("value") Long value) { + Sentry.metrics().gauge("memory.free", value.doubleValue(), MetricsUnit.Information.BYTE); return "gauge metric tracked"; } @GetMapping("distribution/{value}") - String distribution(@PathVariable Long value) { - Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); + String distribution(@PathVariable("value") Long value) { + Sentry.metrics() + .distribution("distributionMetric", value.doubleValue(), MetricsUnit.Duration.MILLISECOND); return "distribution metric tracked"; } } diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java index 5d8a0bf88d..f7c7529525 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java @@ -1,6 +1,7 @@ package io.sentry.samples.spring.boot.jakarta; import io.sentry.Sentry; +import io.sentry.metrics.MetricsUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; @@ -20,14 +21,15 @@ String count() { } @GetMapping("gauge/{value}") - String gauge(@PathVariable Long value) { - Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); + String gauge(@PathVariable("value") Long value) { + Sentry.metrics().gauge("memory.free", value.doubleValue(), MetricsUnit.Information.BYTE); return "gauge metric tracked"; } @GetMapping("distribution/{value}") - String distribution(@PathVariable Long value) { - Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); + String distribution(@PathVariable("value") Long value) { + Sentry.metrics() + .distribution("distributionMetric", value.doubleValue(), MetricsUnit.Duration.MILLISECOND); return "distribution metric tracked"; } } diff --git a/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/MetricController.java b/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/MetricController.java index 5caafc0b26..da5b1d655d 100644 --- a/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/MetricController.java +++ b/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/MetricController.java @@ -1,6 +1,7 @@ package io.sentry.samples.spring.boot; import io.sentry.Sentry; +import io.sentry.metrics.MetricsUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; @@ -20,14 +21,15 @@ String count() { } @GetMapping("gauge/{value}") - String gauge(@PathVariable Long value) { - Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); + String gauge(@PathVariable("value") Long value) { + Sentry.metrics().gauge("memory.free", value.doubleValue(), MetricsUnit.Information.BYTE); return "gauge metric tracked"; } @GetMapping("distribution/{value}") - String distribution(@PathVariable Long value) { - Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); + String distribution(@PathVariable("value") Long value) { + Sentry.metrics() + .distribution("distributionMetric", value.doubleValue(), MetricsUnit.Duration.MILLISECOND); return "distribution metric tracked"; } } diff --git a/sentry-samples/sentry-samples-spring-boot-opentelemetry/src/main/java/io/sentry/samples/spring/boot/MetricController.java b/sentry-samples/sentry-samples-spring-boot-opentelemetry/src/main/java/io/sentry/samples/spring/boot/MetricController.java index 5caafc0b26..da5b1d655d 100644 --- a/sentry-samples/sentry-samples-spring-boot-opentelemetry/src/main/java/io/sentry/samples/spring/boot/MetricController.java +++ b/sentry-samples/sentry-samples-spring-boot-opentelemetry/src/main/java/io/sentry/samples/spring/boot/MetricController.java @@ -1,6 +1,7 @@ package io.sentry.samples.spring.boot; import io.sentry.Sentry; +import io.sentry.metrics.MetricsUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; @@ -20,14 +21,15 @@ String count() { } @GetMapping("gauge/{value}") - String gauge(@PathVariable Long value) { - Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); + String gauge(@PathVariable("value") Long value) { + Sentry.metrics().gauge("memory.free", value.doubleValue(), MetricsUnit.Information.BYTE); return "gauge metric tracked"; } @GetMapping("distribution/{value}") - String distribution(@PathVariable Long value) { - Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); + String distribution(@PathVariable("value") Long value) { + Sentry.metrics() + .distribution("distributionMetric", value.doubleValue(), MetricsUnit.Duration.MILLISECOND); return "distribution metric tracked"; } } diff --git a/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java index 5d8a0bf88d..f7c7529525 100644 --- a/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java +++ b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/MetricController.java @@ -1,6 +1,7 @@ package io.sentry.samples.spring.boot.jakarta; import io.sentry.Sentry; +import io.sentry.metrics.MetricsUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; @@ -20,14 +21,15 @@ String count() { } @GetMapping("gauge/{value}") - String gauge(@PathVariable Long value) { - Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); + String gauge(@PathVariable("value") Long value) { + Sentry.metrics().gauge("memory.free", value.doubleValue(), MetricsUnit.Information.BYTE); return "gauge metric tracked"; } @GetMapping("distribution/{value}") - String distribution(@PathVariable Long value) { - Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); + String distribution(@PathVariable("value") Long value) { + Sentry.metrics() + .distribution("distributionMetric", value.doubleValue(), MetricsUnit.Duration.MILLISECOND); return "distribution metric tracked"; } } diff --git a/sentry-samples/sentry-samples-spring-boot-webflux/src/main/java/io/sentry/samples/spring/boot/MetricController.java b/sentry-samples/sentry-samples-spring-boot-webflux/src/main/java/io/sentry/samples/spring/boot/MetricController.java index 5caafc0b26..da5b1d655d 100644 --- a/sentry-samples/sentry-samples-spring-boot-webflux/src/main/java/io/sentry/samples/spring/boot/MetricController.java +++ b/sentry-samples/sentry-samples-spring-boot-webflux/src/main/java/io/sentry/samples/spring/boot/MetricController.java @@ -1,6 +1,7 @@ package io.sentry.samples.spring.boot; import io.sentry.Sentry; +import io.sentry.metrics.MetricsUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; @@ -20,14 +21,15 @@ String count() { } @GetMapping("gauge/{value}") - String gauge(@PathVariable Long value) { - Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); + String gauge(@PathVariable("value") Long value) { + Sentry.metrics().gauge("memory.free", value.doubleValue(), MetricsUnit.Information.BYTE); return "gauge metric tracked"; } @GetMapping("distribution/{value}") - String distribution(@PathVariable Long value) { - Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); + String distribution(@PathVariable("value") Long value) { + Sentry.metrics() + .distribution("distributionMetric", value.doubleValue(), MetricsUnit.Duration.MILLISECOND); return "distribution metric tracked"; } } diff --git a/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/MetricController.java b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/MetricController.java index 5caafc0b26..da5b1d655d 100644 --- a/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/MetricController.java +++ b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/MetricController.java @@ -1,6 +1,7 @@ package io.sentry.samples.spring.boot; import io.sentry.Sentry; +import io.sentry.metrics.MetricsUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; @@ -20,14 +21,15 @@ String count() { } @GetMapping("gauge/{value}") - String gauge(@PathVariable Long value) { - Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); + String gauge(@PathVariable("value") Long value) { + Sentry.metrics().gauge("memory.free", value.doubleValue(), MetricsUnit.Information.BYTE); return "gauge metric tracked"; } @GetMapping("distribution/{value}") - String distribution(@PathVariable Long value) { - Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); + String distribution(@PathVariable("value") Long value) { + Sentry.metrics() + .distribution("distributionMetric", value.doubleValue(), MetricsUnit.Duration.MILLISECOND); return "distribution metric tracked"; } } diff --git a/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/MetricController.java b/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/MetricController.java index cca33e1ede..6c236a76a9 100644 --- a/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/MetricController.java +++ b/sentry-samples/sentry-samples-spring-jakarta/src/main/java/io/sentry/samples/spring/jakarta/web/MetricController.java @@ -1,6 +1,7 @@ package io.sentry.samples.spring.jakarta.web; import io.sentry.Sentry; +import io.sentry.metrics.MetricsUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; @@ -21,13 +22,14 @@ String count() { @GetMapping("gauge/{value}") String gauge(@PathVariable("value") Long value) { - Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); + Sentry.metrics().gauge("memory.free", value.doubleValue(), MetricsUnit.Information.BYTE); return "gauge metric tracked"; } @GetMapping("distribution/{value}") String distribution(@PathVariable("value") Long value) { - Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); + Sentry.metrics() + .distribution("distributionMetric", value.doubleValue(), MetricsUnit.Duration.MILLISECOND); return "distribution metric tracked"; } } diff --git a/sentry-samples/sentry-samples-spring/src/main/java/io/sentry/samples/spring/web/MetricController.java b/sentry-samples/sentry-samples-spring/src/main/java/io/sentry/samples/spring/web/MetricController.java index a4359d5305..c0629ec137 100644 --- a/sentry-samples/sentry-samples-spring/src/main/java/io/sentry/samples/spring/web/MetricController.java +++ b/sentry-samples/sentry-samples-spring/src/main/java/io/sentry/samples/spring/web/MetricController.java @@ -1,6 +1,7 @@ package io.sentry.samples.spring.web; import io.sentry.Sentry; +import io.sentry.metrics.MetricsUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; @@ -21,13 +22,14 @@ String count() { @GetMapping("gauge/{value}") String gauge(@PathVariable("value") Long value) { - Sentry.metrics().gauge("memory.free", value.doubleValue(), "byte"); + Sentry.metrics().gauge("memory.free", value.doubleValue(), MetricsUnit.Information.BYTE); return "gauge metric tracked"; } @GetMapping("distribution/{value}") String distribution(@PathVariable("value") Long value) { - Sentry.metrics().distribution("distributionMetric", value.doubleValue(), "child"); + Sentry.metrics() + .distribution("distributionMetric", value.doubleValue(), MetricsUnit.Duration.MILLISECOND); return "distribution metric tracked"; } } 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 406b246a82..dfc92d6e92 100644 --- a/sentry-system-test-support/api/sentry-system-test-support.api +++ b/sentry-system-test-support/api/sentry-system-test-support.api @@ -570,7 +570,8 @@ public final class io/sentry/systemtest/util/SentryMockServerClient : io/sentry/ public final class io/sentry/systemtest/util/TestHelper { public fun (Ljava/lang/String;)V public final fun doesContainLogWithBody (Lio/sentry/SentryLogEvents;Ljava/lang/String;)Z - public final fun doesContainMetric (Lio/sentry/SentryMetricsEvents;Ljava/lang/String;Ljava/lang/String;D)Z + public final fun doesContainMetric (Lio/sentry/SentryMetricsEvents;Ljava/lang/String;Ljava/lang/String;DLjava/lang/String;)Z + 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 doesTransactionContainSpanWithDescription (Lio/sentry/protocol/SentryTransaction;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 d6a2c3470d..13d8098d5d 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 @@ -124,10 +124,14 @@ class TestHelper(backendUrl: String) { name: String, type: String, value: Double, + unit: String? = null, ): Boolean { val metricItem = metrics.items.firstOrNull { metricItem -> - metricItem.name == name && metricItem.type == type && metricItem.value == value + metricItem.name == name && + metricItem.type == type && + metricItem.value == value && + (unit == null || metricItem.unit == unit) } if (metricItem == null) { println("Unable to find metric item with name $name, type $type and value $value in metrics:") diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 1970c3fefb..a84a6cf454 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -5257,6 +5257,42 @@ public class io/sentry/metrics/MetricsBatchProcessor : io/sentry/metrics/IMetric public fun flush (J)V } +public final class io/sentry/metrics/MetricsUnit { +} + +public final class io/sentry/metrics/MetricsUnit$Duration { + public static final field DAY Ljava/lang/String; + public static final field HOUR Ljava/lang/String; + public static final field MICROSECOND Ljava/lang/String; + public static final field MILLISECOND Ljava/lang/String; + public static final field MINUTE Ljava/lang/String; + public static final field NANOSECOND Ljava/lang/String; + public static final field SECOND Ljava/lang/String; + public static final field WEEK Ljava/lang/String; +} + +public final class io/sentry/metrics/MetricsUnit$Fraction { + public static final field PERCENT Ljava/lang/String; + public static final field RATIO Ljava/lang/String; +} + +public final class io/sentry/metrics/MetricsUnit$Information { + public static final field BIT Ljava/lang/String; + public static final field BYTE Ljava/lang/String; + public static final field EXABYTE Ljava/lang/String; + public static final field EXBIBYTE Ljava/lang/String; + public static final field GIBIBYTE Ljava/lang/String; + public static final field GIGABYTE Ljava/lang/String; + public static final field KIBIBYTE Ljava/lang/String; + public static final field KILOBYTE Ljava/lang/String; + public static final field MEBIBYTE Ljava/lang/String; + public static final field MEGABYTE Ljava/lang/String; + public static final field PEBIBYTE Ljava/lang/String; + public static final field PETABYTE Ljava/lang/String; + public static final field TEBIBYTE Ljava/lang/String; + public static final field TERABYTE Ljava/lang/String; +} + public final class io/sentry/metrics/NoOpMetricsApi : io/sentry/metrics/IMetricsApi { public fun count (Ljava/lang/String;)V public fun count (Ljava/lang/String;Ljava/lang/Double;)V diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsUnit.java b/sentry/src/main/java/io/sentry/metrics/MetricsUnit.java new file mode 100644 index 0000000000..4665cdf33a --- /dev/null +++ b/sentry/src/main/java/io/sentry/metrics/MetricsUnit.java @@ -0,0 +1,98 @@ +package io.sentry.metrics; + +/** + * String constants for metric units. + * + *

These constants represent the API names of measurement units that can be used with metrics. + */ +public final class MetricsUnit { + + /** Time duration units. */ + public static final class Duration { + /** Nanosecond, 10^-9 seconds. */ + public static final String NANOSECOND = "nanosecond"; + + /** Microsecond, 10^-6 seconds. */ + public static final String MICROSECOND = "microsecond"; + + /** Millisecond, 10^-3 seconds. */ + public static final String MILLISECOND = "millisecond"; + + /** Full second. */ + public static final String SECOND = "second"; + + /** Minute, 60 seconds. */ + public static final String MINUTE = "minute"; + + /** Hour, 3600 seconds. */ + public static final String HOUR = "hour"; + + /** Day, 86,400 seconds. */ + public static final String DAY = "day"; + + /** Week, 604,800 seconds. */ + public static final String WEEK = "week"; + + private Duration() {} + } + + /** Size of information derived from bytes. */ + public static final class Information { + /** Bit, corresponding to 1/8 of a byte. */ + public static final String BIT = "bit"; + + /** Byte. */ + public static final String BYTE = "byte"; + + /** Kilobyte, 10^3 bytes. */ + public static final String KILOBYTE = "kilobyte"; + + /** Kibibyte, 2^10 bytes. */ + public static final String KIBIBYTE = "kibibyte"; + + /** Megabyte, 10^6 bytes. */ + public static final String MEGABYTE = "megabyte"; + + /** Mebibyte, 2^20 bytes. */ + public static final String MEBIBYTE = "mebibyte"; + + /** Gigabyte, 10^9 bytes. */ + public static final String GIGABYTE = "gigabyte"; + + /** Gibibyte, 2^30 bytes. */ + public static final String GIBIBYTE = "gibibyte"; + + /** Terabyte, 10^12 bytes. */ + public static final String TERABYTE = "terabyte"; + + /** Tebibyte, 2^40 bytes. */ + public static final String TEBIBYTE = "tebibyte"; + + /** Petabyte, 10^15 bytes. */ + public static final String PETABYTE = "petabyte"; + + /** Pebibyte, 2^50 bytes. */ + public static final String PEBIBYTE = "pebibyte"; + + /** Exabyte, 10^18 bytes. */ + public static final String EXABYTE = "exabyte"; + + /** Exbibyte, 2^60 bytes. */ + public static final String EXBIBYTE = "exbibyte"; + + private Information() {} + } + + /** Fractions such as percentages. */ + public static final class Fraction { + /** Floating point fraction of `1`. */ + public static final String RATIO = "ratio"; + + /** Ratio expressed as a fraction of `100`. `100%` equals a ratio of `1.0`. */ + public static final String PERCENT = "percent"; + + private Fraction() {} + } + + private MetricsUnit() {} +} diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index 1e22be9877..9975b4435a 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -8,6 +8,7 @@ import io.sentry.clientreport.DiscardedEvent import io.sentry.hints.SessionEndHint import io.sentry.hints.SessionStartHint import io.sentry.logger.SentryLogParameters +import io.sentry.metrics.MetricsUnit import io.sentry.metrics.SentryMetricsParameters import io.sentry.protocol.Feedback import io.sentry.protocol.SentryId @@ -3222,7 +3223,7 @@ class ScopesTest { sut .metrics() - .count("metric name", 1.0, "visit", SentryMetricsParameters().also { it.origin = "other" }) + .count("metric name", 1.0, MetricsUnit.Information.BYTE, SentryMetricsParameters().also { it.origin = "other" }) verify(mockClient) .captureMetric( @@ -3242,14 +3243,14 @@ class ScopesTest { fun `creating count metric with value and unit works`() { val (sut, mockClient) = getEnabledScopes() - sut.metrics().count("metric name", 1.0, "visit") + sut.metrics().count("metric name", 1.0, MetricsUnit.Information.BYTE) verify(mockClient) .captureMetric( check { assertEquals("metric name", it.name) assertEquals(1.0, it.value) - assertEquals("visit", it.unit) + assertEquals(MetricsUnit.Information.BYTE, it.unit) assertEquals("counter", it.type) }, anyOrNull(), @@ -3279,14 +3280,14 @@ class ScopesTest { fun `creating count metric with unit works`() { val (sut, mockClient) = getEnabledScopes() - sut.metrics().count("metric name", "visit") + sut.metrics().count("metric name", MetricsUnit.Information.BYTE) verify(mockClient) .captureMetric( check { assertEquals("metric name", it.name) assertEquals(1.0, it.value) - assertEquals("visit", it.unit) + assertEquals(MetricsUnit.Information.BYTE, it.unit) assertEquals("counter", it.type) }, anyOrNull(), @@ -3303,7 +3304,7 @@ class ScopesTest { .count( "metric name", 1.0, - "visit", + MetricsUnit.Information.BYTE, SentryMetricsParameters.create(SentryAttributes.fromMap(mapOf("attrname1" to "attrval1"))), ) @@ -3312,7 +3313,7 @@ class ScopesTest { check { assertEquals("metric name", it.name) assertEquals(1.0, it.value) - assertEquals("visit", it.unit) + assertEquals(MetricsUnit.Information.BYTE, it.unit) assertEquals("counter", it.type) val attr1 = it.attributes?.get("attrname1")!! @@ -3333,7 +3334,7 @@ class ScopesTest { .count( "metric name", 1.0, - "visit", + MetricsUnit.Information.BYTE, SentryMetricsParameters.create(mapOf("attrname1" to "attrval1")), ) @@ -3342,7 +3343,7 @@ class ScopesTest { check { assertEquals("metric name", it.name) assertEquals(1.0, it.value) - assertEquals("visit", it.unit) + assertEquals(MetricsUnit.Information.BYTE, it.unit) assertEquals("counter", it.type) val attr1 = it.attributes?.get("attrname1")!! @@ -3363,7 +3364,7 @@ class ScopesTest { .count( "metric name", 1.0, - "visit", + MetricsUnit.Information.BYTE, SentryMetricsParameters.create( SentryAttributes.of( SentryAttribute.stringAttribute("strattr", "strval"), @@ -3383,7 +3384,7 @@ class ScopesTest { check { assertEquals("metric name", it.name) assertEquals(1.0, it.value) - assertEquals("visit", it.unit) + assertEquals(MetricsUnit.Information.BYTE, it.unit) assertEquals("counter", it.type) val strattr = it.attributes?.get("strattr")!! @@ -3432,7 +3433,7 @@ class ScopesTest { .count( "metric name", 1.0, - "visit", + MetricsUnit.Information.BYTE, SentryMetricsParameters.create( SentryLongDate(123), SentryAttributes.of(SentryAttribute.named("attrname1", "attrval1")), @@ -3444,7 +3445,7 @@ class ScopesTest { check { assertEquals("metric name", it.name) assertEquals(1.0, it.value) - assertEquals("visit", it.unit) + assertEquals(MetricsUnit.Information.BYTE, it.unit) assertEquals("counter", it.type) val attr1 = it.attributes?.get("attrname1")!! @@ -3460,14 +3461,14 @@ class ScopesTest { fun `creating distribution metric with value and unit works`() { val (sut, mockClient) = getEnabledScopes() - sut.metrics().distribution("metric name", 1.0, "ms") + sut.metrics().distribution("metric name", 1.0, MetricsUnit.Duration.MILLISECOND) verify(mockClient) .captureMetric( check { assertEquals("metric name", it.name) assertEquals(1.0, it.value) - assertEquals("ms", it.unit) + assertEquals(MetricsUnit.Duration.MILLISECOND, it.unit) assertEquals("distribution", it.type) }, anyOrNull(), @@ -3502,7 +3503,7 @@ class ScopesTest { .distribution( "metric name", 3.7, - "ms", + MetricsUnit.Duration.MILLISECOND, SentryMetricsParameters.create(SentryAttributes.fromMap(mapOf("attrname1" to "attrval1"))), ) @@ -3511,7 +3512,7 @@ class ScopesTest { check { assertEquals("metric name", it.name) assertEquals(3.7, it.value) - assertEquals("ms", it.unit) + assertEquals(MetricsUnit.Duration.MILLISECOND, it.unit) assertEquals("distribution", it.type) val attr1 = it.attributes?.get("attrname1")!! @@ -3532,7 +3533,7 @@ class ScopesTest { .distribution( "metric name", 3.7, - "ms", + MetricsUnit.Duration.MILLISECOND, SentryMetricsParameters.create( SentryAttributes.of( SentryAttribute.stringAttribute("strattr", "strval"), @@ -3552,7 +3553,7 @@ class ScopesTest { check { assertEquals("metric name", it.name) assertEquals(3.7, it.value) - assertEquals("ms", it.unit) + assertEquals(MetricsUnit.Duration.MILLISECOND, it.unit) assertEquals("distribution", it.type) val strattr = it.attributes?.get("strattr")!! @@ -3601,7 +3602,7 @@ class ScopesTest { .distribution( "metric name", 3.7, - "ms", + MetricsUnit.Duration.MILLISECOND, SentryMetricsParameters.create( SentryLongDate(123), SentryAttributes.of(SentryAttribute.named("attrname1", "attrval1")), @@ -3613,7 +3614,7 @@ class ScopesTest { check { assertEquals("metric name", it.name) assertEquals(3.7, it.value) - assertEquals("ms", it.unit) + assertEquals(MetricsUnit.Duration.MILLISECOND, it.unit) assertEquals("distribution", it.type) val attr1 = it.attributes?.get("attrname1")!! @@ -3629,14 +3630,14 @@ class ScopesTest { fun `creating gauge metric with value and unit works`() { val (sut, mockClient) = getEnabledScopes() - sut.metrics().gauge("metric name", 128.0, "byte") + sut.metrics().gauge("metric name", 128.0, MetricsUnit.Information.BYTE) verify(mockClient) .captureMetric( check { assertEquals("metric name", it.name) assertEquals(128.0, it.value) - assertEquals("byte", it.unit) + assertEquals(MetricsUnit.Information.BYTE, it.unit) assertEquals("gauge", it.type) }, anyOrNull(), @@ -3671,7 +3672,7 @@ class ScopesTest { .gauge( "metric name", 256.0, - "byte", + MetricsUnit.Information.BYTE, SentryMetricsParameters.create(SentryAttributes.fromMap(mapOf("attrname1" to "attrval1"))), ) @@ -3680,7 +3681,7 @@ class ScopesTest { check { assertEquals("metric name", it.name) assertEquals(256.0, it.value) - assertEquals("byte", it.unit) + assertEquals(MetricsUnit.Information.BYTE, it.unit) assertEquals("gauge", it.type) val attr1 = it.attributes?.get("attrname1")!! @@ -3701,7 +3702,7 @@ class ScopesTest { .gauge( "metric name", 256.0, - "byte", + MetricsUnit.Information.BYTE, SentryMetricsParameters.create( SentryAttributes.of( SentryAttribute.stringAttribute("strattr", "strval"), @@ -3721,7 +3722,7 @@ class ScopesTest { check { assertEquals("metric name", it.name) assertEquals(256.0, it.value) - assertEquals("byte", it.unit) + assertEquals(MetricsUnit.Information.BYTE, it.unit) assertEquals("gauge", it.type) val strattr = it.attributes?.get("strattr")!! @@ -3770,7 +3771,7 @@ class ScopesTest { .gauge( "metric name", 256.0, - "byte", + MetricsUnit.Information.BYTE, SentryMetricsParameters.create( SentryLongDate(123), SentryAttributes.of(SentryAttribute.named("attrname1", "attrval1")), @@ -3782,7 +3783,7 @@ class ScopesTest { check { assertEquals("metric name", it.name) assertEquals(256.0, it.value) - assertEquals("byte", it.unit) + assertEquals(MetricsUnit.Information.BYTE, it.unit) assertEquals("gauge", it.type) val attr1 = it.attributes?.get("attrname1")!! From 735f8f1a271400c9ed2403acfdf14edcc12a7e0b Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Tue, 13 Jan 2026 13:36:52 +0000 Subject: [PATCH 34/40] Format code --- sentry/src/test/java/io/sentry/ScopesTest.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index 9975b4435a..81f639e8a1 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -3223,7 +3223,12 @@ class ScopesTest { sut .metrics() - .count("metric name", 1.0, MetricsUnit.Information.BYTE, SentryMetricsParameters().also { it.origin = "other" }) + .count( + "metric name", + 1.0, + MetricsUnit.Information.BYTE, + SentryMetricsParameters().also { it.origin = "other" }, + ) verify(mockClient) .captureMetric( From e7b20a08882fd6284668a65a676c47fec65f9003 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 14 Jan 2026 10:09:06 +0100 Subject: [PATCH 35/40] Metrics Options (#4980) --- sentry/api/sentry.api | 18 +++- .../main/java/io/sentry/SentryOptions.java | 87 ++++++++++++++++--- 2 files changed, 87 insertions(+), 18 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index f6729869eb..b72982774d 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -3420,6 +3420,7 @@ public class io/sentry/SentryOptions { public fun getMaxRequestBodySize ()Lio/sentry/SentryOptions$RequestSize; public fun getMaxSpans ()I public fun getMaxTraceFileSize ()J + public fun getMetrics ()Lio/sentry/SentryOptions$Metrics; public fun getModulesLoader ()Lio/sentry/internal/modules/IModulesLoader; public fun getOnDiscard ()Lio/sentry/SentryOptions$OnDiscardCallback; public fun getOnOversizedEvent ()Lio/sentry/SentryOptions$OnOversizedEventCallback; @@ -3572,6 +3573,7 @@ public class io/sentry/SentryOptions { public fun setMaxRequestBodySize (Lio/sentry/SentryOptions$RequestSize;)V public fun setMaxSpans (I)V public fun setMaxTraceFileSize (J)V + public fun setMetrics (Lio/sentry/SentryOptions$Metrics;)V public fun setModulesLoader (Lio/sentry/internal/modules/IModulesLoader;)V public fun setOnDiscard (Lio/sentry/SentryOptions$OnDiscardCallback;)V public fun setOnOversizedEvent (Lio/sentry/SentryOptions$OnOversizedEventCallback;)V @@ -3626,10 +3628,6 @@ public abstract interface class io/sentry/SentryOptions$BeforeBreadcrumbCallback public abstract fun execute (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)Lio/sentry/Breadcrumb; } -public abstract interface class io/sentry/SentryOptions$BeforeEmitMetricCallback { - public abstract fun execute (Ljava/lang/String;Ljava/util/Map;)Z -} - public abstract interface class io/sentry/SentryOptions$BeforeEnvelopeCallback { public abstract fun execute (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V } @@ -3683,6 +3681,18 @@ public abstract interface class io/sentry/SentryOptions$Logs$BeforeSendLogCallba public abstract fun execute (Lio/sentry/SentryLogEvent;)Lio/sentry/SentryLogEvent; } +public final class io/sentry/SentryOptions$Metrics { + public fun ()V + public fun getBeforeSend ()Lio/sentry/SentryOptions$Metrics$BeforeSendMetricCallback; + public fun isEnabled ()Z + public fun setBeforeSend (Lio/sentry/SentryOptions$Metrics$BeforeSendMetricCallback;)V + public fun setEnabled (Z)V +} + +public abstract interface class io/sentry/SentryOptions$Metrics$BeforeSendMetricCallback { + public abstract fun execute (Ljava/lang/Object;)Ljava/lang/Object; +} + public abstract interface class io/sentry/SentryOptions$OnDiscardCallback { public abstract fun execute (Lio/sentry/clientreport/DiscardReason;Lio/sentry/DataCategory;Ljava/lang/Long;)V } diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 835411cb61..2575328c70 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -624,6 +624,8 @@ public class SentryOptions { private @NotNull SentryOptions.Logs logs = new SentryOptions.Logs(); + private @NotNull SentryOptions.Metrics metrics = new SentryOptions.Metrics(); + private @NotNull ISocketTagger socketTagger = NoOpSocketTagger.getInstance(); /** Runtime manager to manage runtime policies, like StrictMode on Android. */ @@ -3261,20 +3263,6 @@ public interface BeforeEnvelopeCallback { void execute(@NotNull SentryEnvelope envelope, @Nullable Hint hint); } - /** The BeforeEmitMetric callback */ - @ApiStatus.Experimental - public interface BeforeEmitMetricCallback { - - /** - * A callback which gets called right before a metric is about to be emitted. - * - * @param key the metric key - * @param tags the metric tags - * @return true if the metric should be emitted, false otherwise - */ - boolean execute(@NotNull String key, @Nullable Map tags); - } - /** * Creates SentryOptions instance without initializing any of the internal parts. * @@ -3537,6 +3525,16 @@ public void setLogs(@NotNull SentryOptions.Logs logs) { this.logs = logs; } + @ApiStatus.Experimental + public @NotNull SentryOptions.Metrics getMetrics() { + return metrics; + } + + @ApiStatus.Experimental + public void setMetrics(@NotNull SentryOptions.Metrics metrics) { + this.metrics = metrics; + } + public static final class Proxy { private @Nullable String host; private @Nullable String port; @@ -3741,6 +3739,67 @@ public interface BeforeSendLogCallback { } } + public static final class Metrics { + + /** Whether Sentry Metrics feature is enabled and metrics are sent to Sentry. */ + private boolean enable = false; + + /** + * This function is called with a metric key and tags and can return false to skip sending the + * metric + */ + private @Nullable BeforeSendMetricCallback beforeSend; + + /** + * Whether Sentry Metrics feature is enabled and metrics are sent to Sentry. + * + * @return true if Sentry Metrics should be enabled + */ + public boolean isEnabled() { + return enable; + } + + /** + * Whether Sentry Metrics feature is enabled and metrics are sent to Sentry. + * + * @param enableMetrics true if Sentry Metrics should be enabled + */ + public void setEnabled(boolean enableMetrics) { + this.enable = enableMetrics; + } + + /** + * Returns the BeforeSendMetric callback + * + * @return the beforeSend callback or null if not set + */ + public @Nullable BeforeSendMetricCallback getBeforeSend() { + return beforeSend; + } + + /** + * Sets the beforeSend callback for metrics + * + * @param beforeSend the beforeSend callback for metrics + */ + public void setBeforeSend(@Nullable BeforeSendMetricCallback beforeSend) { + this.beforeSend = beforeSend; + } + + public interface BeforeSendMetricCallback { + + /** + * A callback which gets called right before a metric is about to be sent. + * + * @param metric the metric + * @return the original metric, mutated metric or null if metric was dropped + */ + // TODO replace with SentryMetric + @Nullable + Object execute(@NotNull Object metric); + } + } + @ApiStatus.Experimental public @NotNull DistributionOptions getDistribution() { return distribution; From 1c969993ecab212cc8816547a6a305a06fdfb9d1 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 14 Jan 2026 10:12:33 +0100 Subject: [PATCH 36/40] feat(metrics): [Trace Metrics 2] MetricsApi stub (#4981) * Metrics Options * MetricsApi stub --- sentry/api/sentry.api | 22 +++++++++++++++++++ .../src/main/java/io/sentry/HubAdapter.java | 6 +++++ .../main/java/io/sentry/HubScopesWrapper.java | 6 +++++ sentry/src/main/java/io/sentry/IScopes.java | 4 ++++ sentry/src/main/java/io/sentry/NoOpHub.java | 7 ++++++ .../src/main/java/io/sentry/NoOpScopes.java | 7 ++++++ sentry/src/main/java/io/sentry/Scopes.java | 9 ++++++++ .../main/java/io/sentry/ScopesAdapter.java | 6 +++++ sentry/src/main/java/io/sentry/Sentry.java | 6 +++++ .../java/io/sentry/metrics/IMetricsApi.java | 11 ++++++++++ .../java/io/sentry/metrics/MetricsApi.java | 18 +++++++++++++++ .../io/sentry/metrics/NoOpMetricsApi.java | 16 ++++++++++++++ 12 files changed, 118 insertions(+) create mode 100644 sentry/src/main/java/io/sentry/metrics/IMetricsApi.java create mode 100644 sentry/src/main/java/io/sentry/metrics/MetricsApi.java create mode 100644 sentry/src/main/java/io/sentry/metrics/NoOpMetricsApi.java diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index b72982774d..f947368b75 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -661,6 +661,7 @@ public final class io/sentry/HubAdapter : io/sentry/IHub { public fun isHealthy ()Z public fun logger ()Lio/sentry/logger/ILoggerApi; public fun makeCurrent ()Lio/sentry/ISentryLifecycleToken; + public fun metrics ()Lio/sentry/metrics/IMetricsApi; public fun popScope ()V public fun pushIsolationScope ()Lio/sentry/ISentryLifecycleToken; public fun pushScope ()Lio/sentry/ISentryLifecycleToken; @@ -732,6 +733,7 @@ public final class io/sentry/HubScopesWrapper : io/sentry/IHub { public fun isHealthy ()Z public fun logger ()Lio/sentry/logger/ILoggerApi; public fun makeCurrent ()Lio/sentry/ISentryLifecycleToken; + public fun metrics ()Lio/sentry/metrics/IMetricsApi; public fun popScope ()V public fun pushIsolationScope ()Lio/sentry/ISentryLifecycleToken; public fun pushScope ()Lio/sentry/ISentryLifecycleToken; @@ -992,6 +994,7 @@ public abstract interface class io/sentry/IScopes { public fun isNoOp ()Z public abstract fun logger ()Lio/sentry/logger/ILoggerApi; public abstract fun makeCurrent ()Lio/sentry/ISentryLifecycleToken; + public abstract fun metrics ()Lio/sentry/metrics/IMetricsApi; public abstract fun popScope ()V public abstract fun pushIsolationScope ()Lio/sentry/ISentryLifecycleToken; public abstract fun pushScope ()Lio/sentry/ISentryLifecycleToken; @@ -1565,6 +1568,7 @@ public final class io/sentry/NoOpHub : io/sentry/IHub { public fun isNoOp ()Z public fun logger ()Lio/sentry/logger/ILoggerApi; public fun makeCurrent ()Lio/sentry/ISentryLifecycleToken; + public fun metrics ()Lio/sentry/metrics/IMetricsApi; public fun popScope ()V public fun pushIsolationScope ()Lio/sentry/ISentryLifecycleToken; public fun pushScope ()Lio/sentry/ISentryLifecycleToken; @@ -1741,6 +1745,7 @@ public final class io/sentry/NoOpScopes : io/sentry/IScopes { public fun isNoOp ()Z public fun logger ()Lio/sentry/logger/ILoggerApi; public fun makeCurrent ()Lio/sentry/ISentryLifecycleToken; + public fun metrics ()Lio/sentry/metrics/IMetricsApi; public fun popScope ()V public fun pushIsolationScope ()Lio/sentry/ISentryLifecycleToken; public fun pushScope ()Lio/sentry/ISentryLifecycleToken; @@ -2456,6 +2461,7 @@ public final class io/sentry/Scopes : io/sentry/IScopes { public fun isHealthy ()Z public fun logger ()Lio/sentry/logger/ILoggerApi; public fun makeCurrent ()Lio/sentry/ISentryLifecycleToken; + public fun metrics ()Lio/sentry/metrics/IMetricsApi; public fun popScope ()V public fun pushIsolationScope ()Lio/sentry/ISentryLifecycleToken; public fun pushScope ()Lio/sentry/ISentryLifecycleToken; @@ -2528,6 +2534,7 @@ public final class io/sentry/ScopesAdapter : io/sentry/IScopes { public fun isHealthy ()Z public fun logger ()Lio/sentry/logger/ILoggerApi; public fun makeCurrent ()Lio/sentry/ISentryLifecycleToken; + public fun metrics ()Lio/sentry/metrics/IMetricsApi; public fun popScope ()V public fun pushIsolationScope ()Lio/sentry/ISentryLifecycleToken; public fun pushScope ()Lio/sentry/ISentryLifecycleToken; @@ -2648,6 +2655,7 @@ public final class io/sentry/Sentry { public static fun isEnabled ()Z public static fun isHealthy ()Z public static fun logger ()Lio/sentry/logger/ILoggerApi; + public static fun metrics ()Lio/sentry/metrics/IMetricsApi; public static fun popScope ()V public static fun pushIsolationScope ()Lio/sentry/ISentryLifecycleToken; public static fun pushScope ()Lio/sentry/ISentryLifecycleToken; @@ -5123,6 +5131,20 @@ public final class io/sentry/logger/SentryLogParameters { public fun setTimestamp (Lio/sentry/SentryDate;)V } +public abstract interface class io/sentry/metrics/IMetricsApi { + public abstract fun count (Ljava/lang/String;)V +} + +public final class io/sentry/metrics/MetricsApi : io/sentry/metrics/IMetricsApi { + public fun (Lio/sentry/Scopes;)V + public fun count (Ljava/lang/String;)V +} + +public final class io/sentry/metrics/NoOpMetricsApi : io/sentry/metrics/IMetricsApi { + public fun count (Ljava/lang/String;)V + public static fun getInstance ()Lio/sentry/metrics/NoOpMetricsApi; +} + public final class io/sentry/opentelemetry/OpenTelemetryUtil { public fun ()V public static fun applyIgnoredSpanOrigins (Lio/sentry/SentryOptions;)V diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index 31a2e219cd..5715d06114 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -1,6 +1,7 @@ package io.sentry; import io.sentry.logger.ILoggerApi; +import io.sentry.metrics.IMetricsApi; import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; @@ -389,6 +390,11 @@ public void reportFullyDisplayed() { return Sentry.getCurrentScopes().logger(); } + @Override + public @NotNull IMetricsApi metrics() { + return Sentry.getCurrentScopes().metrics(); + } + @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 d15ed72ee4..c04aad9ed8 100644 --- a/sentry/src/main/java/io/sentry/HubScopesWrapper.java +++ b/sentry/src/main/java/io/sentry/HubScopesWrapper.java @@ -1,6 +1,7 @@ package io.sentry; import io.sentry.logger.ILoggerApi; +import io.sentry.metrics.IMetricsApi; import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; @@ -374,6 +375,11 @@ public void reportFullyDisplayed() { return scopes.logger(); } + @Override + public @NotNull IMetricsApi metrics() { + return scopes.metrics(); + } + @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/IScopes.java b/sentry/src/main/java/io/sentry/IScopes.java index bf78d28ecd..0a7c86fa8e 100644 --- a/sentry/src/main/java/io/sentry/IScopes.java +++ b/sentry/src/main/java/io/sentry/IScopes.java @@ -1,6 +1,7 @@ package io.sentry; import io.sentry.logger.ILoggerApi; +import io.sentry.metrics.IMetricsApi; import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; @@ -744,5 +745,8 @@ default boolean isNoOp() { @NotNull ILoggerApi logger(); + @NotNull + IMetricsApi metrics(); + 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 811e1d297a..2885d8017d 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -2,6 +2,8 @@ import io.sentry.logger.ILoggerApi; import io.sentry.logger.NoOpLoggerApi; +import io.sentry.metrics.IMetricsApi; +import io.sentry.metrics.NoOpMetricsApi; import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; @@ -331,6 +333,11 @@ public boolean isNoOp() { return NoOpLoggerApi.getInstance(); } + @Override + public @NotNull IMetricsApi metrics() { + return NoOpMetricsApi.getInstance(); + } + @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 40777da892..5abb20226a 100644 --- a/sentry/src/main/java/io/sentry/NoOpScopes.java +++ b/sentry/src/main/java/io/sentry/NoOpScopes.java @@ -2,6 +2,8 @@ import io.sentry.logger.ILoggerApi; import io.sentry.logger.NoOpLoggerApi; +import io.sentry.metrics.IMetricsApi; +import io.sentry.metrics.NoOpMetricsApi; import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; @@ -329,6 +331,11 @@ public boolean isNoOp() { return NoOpLoggerApi.getInstance(); } + @Override + public @NotNull IMetricsApi metrics() { + return NoOpMetricsApi.getInstance(); + } + @Override public void addFeatureFlag(final @Nullable String flag, final @Nullable Boolean result) {} } diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index c8afde59cc..374ecbfdb5 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -5,6 +5,8 @@ import io.sentry.hints.SessionStartHint; import io.sentry.logger.ILoggerApi; import io.sentry.logger.LoggerApi; +import io.sentry.metrics.IMetricsApi; +import io.sentry.metrics.MetricsApi; import io.sentry.protocol.*; import io.sentry.transport.RateLimiter; import io.sentry.util.HintUtils; @@ -31,6 +33,7 @@ public final class Scopes implements IScopes { private final @NotNull CombinedScopeView combinedScope; private final @NotNull ILoggerApi logger; + private final @NotNull IMetricsApi metrics; public Scopes( final @NotNull IScope scope, @@ -57,6 +60,7 @@ private Scopes( validateOptions(options); this.compositePerformanceCollector = options.getCompositePerformanceCollector(); this.logger = new LoggerApi(this); + this.metrics = new MetricsApi(this); } public @NotNull String getCreator() { @@ -1220,6 +1224,11 @@ public void reportFullyDisplayed() { return logger; } + @Override + public @NotNull IMetricsApi metrics() { + return metrics; + } + @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 99e70694ee..ba7e74d23b 100644 --- a/sentry/src/main/java/io/sentry/ScopesAdapter.java +++ b/sentry/src/main/java/io/sentry/ScopesAdapter.java @@ -1,6 +1,7 @@ package io.sentry; import io.sentry.logger.ILoggerApi; +import io.sentry.metrics.IMetricsApi; import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; @@ -386,6 +387,11 @@ public void reportFullyDisplayed() { return Sentry.getCurrentScopes().logger(); } + @Override + public @NotNull IMetricsApi metrics() { + return Sentry.getCurrentScopes().metrics(); + } + @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 f726c1a602..178f97a3b3 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -14,6 +14,7 @@ import io.sentry.internal.modules.NoOpModulesLoader; import io.sentry.internal.modules.ResourcesModulesLoader; import io.sentry.logger.ILoggerApi; +import io.sentry.metrics.IMetricsApi; import io.sentry.opentelemetry.OpenTelemetryUtil; import io.sentry.protocol.Feedback; import io.sentry.protocol.SentryId; @@ -1344,6 +1345,11 @@ public static IDistributionApi distribution() { return getCurrentScopes().getScope().getOptions().getDistributionController(); } + @NotNull + public static IMetricsApi metrics() { + return getCurrentScopes().metrics(); + } + public static void showUserFeedbackDialog() { showUserFeedbackDialog(null); } diff --git a/sentry/src/main/java/io/sentry/metrics/IMetricsApi.java b/sentry/src/main/java/io/sentry/metrics/IMetricsApi.java new file mode 100644 index 0000000000..ddd6bb9d41 --- /dev/null +++ b/sentry/src/main/java/io/sentry/metrics/IMetricsApi.java @@ -0,0 +1,11 @@ +package io.sentry.metrics; + +import org.jetbrains.annotations.NotNull; + +public interface IMetricsApi { + + void count(@NotNull final String name); + // distribution + // gauge + // +} diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java new file mode 100644 index 0000000000..7b878de3d2 --- /dev/null +++ b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java @@ -0,0 +1,18 @@ +package io.sentry.metrics; + +import io.sentry.Scopes; +import org.jetbrains.annotations.NotNull; + +public final class MetricsApi implements IMetricsApi { + + private final @NotNull Scopes scopes; + + public MetricsApi(final @NotNull Scopes scopes) { + this.scopes = scopes; + } + + @Override + public void count(@NotNull String name) { + scopes.getOptions(); + } +} diff --git a/sentry/src/main/java/io/sentry/metrics/NoOpMetricsApi.java b/sentry/src/main/java/io/sentry/metrics/NoOpMetricsApi.java new file mode 100644 index 0000000000..f6977b6953 --- /dev/null +++ b/sentry/src/main/java/io/sentry/metrics/NoOpMetricsApi.java @@ -0,0 +1,16 @@ +package io.sentry.metrics; + +import org.jetbrains.annotations.NotNull; + +public final class NoOpMetricsApi implements IMetricsApi { + private static final NoOpMetricsApi instance = new NoOpMetricsApi(); + + private NoOpMetricsApi() {} + + public static NoOpMetricsApi getInstance() { + return instance; + } + + @Override + public void count(@NotNull String name) {} +} From 0d163640753d5c43854697da4a6d76a013b959e6 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 14 Jan 2026 14:09:44 +0100 Subject: [PATCH 37/40] More PR review changes (#5028) * Some more PR review changes * Format code --------- Co-authored-by: Sentry Github Bot --- .cursor/rules/metrics.mdc | 4 ++-- sentry/src/main/java/io/sentry/SentryMetricsEvent.java | 8 ++++++++ .../main/java/io/sentry/logger/LoggerBatchProcessor.java | 3 +-- .../java/io/sentry/metrics/MetricsBatchProcessor.java | 3 +-- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.cursor/rules/metrics.mdc b/.cursor/rules/metrics.mdc index 8d5c803fa4..93c82ecb46 100644 --- a/.cursor/rules/metrics.mdc +++ b/.cursor/rules/metrics.mdc @@ -1,8 +1,8 @@ --- alwaysApply: false -description: Metrics +description: Metrics API --- -# Java SDK Metrics +# Java SDK Metrics API Metrics are enabled by default. diff --git a/sentry/src/main/java/io/sentry/SentryMetricsEvent.java b/sentry/src/main/java/io/sentry/SentryMetricsEvent.java index 81723afb6e..a55e39e593 100644 --- a/sentry/src/main/java/io/sentry/SentryMetricsEvent.java +++ b/sentry/src/main/java/io/sentry/SentryMetricsEvent.java @@ -107,6 +107,14 @@ public void setSpanId(final @Nullable SpanId spanId) { this.spanId = spanId; } + public @Nullable SentryId getTraceId() { + return traceId; + } + + public void setTraceId(final @Nullable SentryId traceId) { + this.traceId = traceId; + } + public @NotNull Double getValue() { return value; } diff --git a/sentry/src/main/java/io/sentry/logger/LoggerBatchProcessor.java b/sentry/src/main/java/io/sentry/logger/LoggerBatchProcessor.java index cdea169b92..d4bb7e9afb 100644 --- a/sentry/src/main/java/io/sentry/logger/LoggerBatchProcessor.java +++ b/sentry/src/main/java/io/sentry/logger/LoggerBatchProcessor.java @@ -36,8 +36,7 @@ public class LoggerBatchProcessor implements ILoggerBatchProcessor { private final @NotNull Queue queue; private final @NotNull ISentryExecutorService executorService; private volatile @Nullable Future scheduledFlush; - private static final @NotNull AutoClosableReentrantLock scheduleLock = - new AutoClosableReentrantLock(); + private final @NotNull AutoClosableReentrantLock scheduleLock = new AutoClosableReentrantLock(); private volatile boolean hasScheduled = false; private final @NotNull ReusableCountLatch pendingCount = new ReusableCountLatch(); diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsBatchProcessor.java b/sentry/src/main/java/io/sentry/metrics/MetricsBatchProcessor.java index 0b736a0353..169f36b973 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsBatchProcessor.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsBatchProcessor.java @@ -35,8 +35,7 @@ public class MetricsBatchProcessor implements IMetricsBatchProcessor { private final @NotNull Queue queue; private final @NotNull ISentryExecutorService executorService; private volatile @Nullable Future scheduledFlush; - private static final @NotNull AutoClosableReentrantLock scheduleLock = - new AutoClosableReentrantLock(); + private final @NotNull AutoClosableReentrantLock scheduleLock = new AutoClosableReentrantLock(); private volatile boolean hasScheduled = false; private volatile boolean isShuttingDown = false; From de58ea07dc2f115f280c478a56a539c113cfff3b Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 14 Jan 2026 14:56:33 +0100 Subject: [PATCH 38/40] changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d6b0e37e6..8fb05bc617 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,13 @@ - Add a Tombstone integration that detects native crashes without relying on the NDK integration, but instead using `ApplicationExitInfo.REASON_CRASH_NATIVE` on Android 12+. ([#4933](https://github.com/getsentry/sentry-java/pull/4933)) - Currently exposed via options as an _internal_ API only. - If enabled alongside the NDK integration, crashes will be reported as two separate events. Users should enable only one; deduplication between both integrations will be added in a future release. +- Add Sentry Metrics to Java SDK ([#5026](https://github.com/getsentry/sentry-java/pull/5026)) + - Metrics are enabled by default + - APIs are namespaced under `Sentry.metrics()` + - We offer the following APIs: + - `count`: A metric that increments counts + - `gauge`: A metric that tracks a value that can go up or down + - `distribution`: A metric that tracks the statistical distribution of values ## 8.29.0 From e8963d826374083fd23e1e4b5679af659ee0f631 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 14 Jan 2026 15:03:16 +0100 Subject: [PATCH 39/40] non nullable trace id --- sentry/api/sentry.api | 2 ++ sentry/src/main/java/io/sentry/SentryMetricsEvent.java | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index a84a6cf454..c8e0ec4bde 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -3338,6 +3338,7 @@ public final class io/sentry/SentryMetricsEvent : io/sentry/JsonSerializable, io public fun getName ()Ljava/lang/String; public fun getSpanId ()Lio/sentry/SpanId; public fun getTimestamp ()Ljava/lang/Double; + public fun getTraceId ()Lio/sentry/protocol/SentryId; public fun getType ()Ljava/lang/String; public fun getUnit ()Ljava/lang/String; public fun getUnknown ()Ljava/util/Map; @@ -3348,6 +3349,7 @@ public final class io/sentry/SentryMetricsEvent : io/sentry/JsonSerializable, io public fun setName (Ljava/lang/String;)V public fun setSpanId (Lio/sentry/SpanId;)V public fun setTimestamp (Ljava/lang/Double;)V + public fun setTraceId (Lio/sentry/protocol/SentryId;)V public fun setType (Ljava/lang/String;)V public fun setUnit (Ljava/lang/String;)V public fun setUnknown (Ljava/util/Map;)V diff --git a/sentry/src/main/java/io/sentry/SentryMetricsEvent.java b/sentry/src/main/java/io/sentry/SentryMetricsEvent.java index a55e39e593..b04c83a4d0 100644 --- a/sentry/src/main/java/io/sentry/SentryMetricsEvent.java +++ b/sentry/src/main/java/io/sentry/SentryMetricsEvent.java @@ -107,11 +107,11 @@ public void setSpanId(final @Nullable SpanId spanId) { this.spanId = spanId; } - public @Nullable SentryId getTraceId() { + public @NotNull SentryId getTraceId() { return traceId; } - public void setTraceId(final @Nullable SentryId traceId) { + public void setTraceId(final @NotNull SentryId traceId) { this.traceId = traceId; } From 11a2ea8801f6be9a8c9efb47dfc8954add51e97c Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 14 Jan 2026 15:03:34 +0100 Subject: [PATCH 40/40] error log now says metric instead of log --- sentry/src/main/java/io/sentry/metrics/MetricsApi.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java index 9dc28dd5b1..08a1510049 100644 --- a/sentry/src/main/java/io/sentry/metrics/MetricsApi.java +++ b/sentry/src/main/java/io/sentry/metrics/MetricsApi.java @@ -160,7 +160,7 @@ private void captureMetrics( scopes.getClient().captureMetric(metricsEvent, combinedScope, params.getHint()); } catch (Throwable e) { - options.getLogger().log(SentryLevel.ERROR, "Error while capturing log event", e); + options.getLogger().log(SentryLevel.ERROR, "Error while capturing metrics event", e); } }