From 1df9f124db883bf988f1e7d497058f8346719a04 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 18 Dec 2025 13:55:10 +0100 Subject: [PATCH] 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 b4235aa6b91..1a2f0db30c7 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 9a8f3d68673..85c1e6625a9 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 b258132edf3..4f59de75789 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 cd9ce85e9ad..79172be060c 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 cd2ed885ca8..47b33438742 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 ed6c5cbe036..e67a6a6aea5 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 e832a307266..724ee727efd 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); } }