diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 20ebc767ee..dd4cfdf353 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -37,10 +37,10 @@ public final class io/sentry/android/core/ActivityLifecycleIntegration : android } public class io/sentry/android/core/AndroidContinuousProfiler : io/sentry/IContinuousProfiler { - public fun (Lio/sentry/android/core/BuildInfoProvider;Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;Lio/sentry/ILogger;Ljava/lang/String;ZILio/sentry/ISentryExecutorService;)V + public fun (Lio/sentry/android/core/BuildInfoProvider;Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;Lio/sentry/ILogger;Ljava/lang/String;ILio/sentry/ISentryExecutorService;)V public fun close ()V + public fun getProfilerId ()Lio/sentry/protocol/SentryId; public fun isRunning ()Z - public fun setScopes (Lio/sentry/IScopes;)V public fun start ()V public fun stop ()V } @@ -447,6 +447,7 @@ public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/andr public fun addActivityLifecycleTimeSpans (Lio/sentry/android/core/performance/ActivityLifecycleTimeSpan;)V public fun clear ()V public fun getActivityLifecycleTimeSpans ()Ljava/util/List; + public fun getAppStartContinuousProfiler ()Lio/sentry/IContinuousProfiler; public fun getAppStartProfiler ()Lio/sentry/ITransactionProfiler; public fun getAppStartSamplingDecision ()Lio/sentry/TracesSamplingDecision; public fun getAppStartTimeSpan ()Lio/sentry/android/core/performance/TimeSpan; @@ -465,6 +466,7 @@ public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/andr public static fun onContentProviderPostCreate (Landroid/content/ContentProvider;)V public fun registerApplicationForegroundCheck (Landroid/app/Application;)V public fun setAppLaunchedInForeground (Z)V + public fun setAppStartContinuousProfiler (Lio/sentry/IContinuousProfiler;)V public fun setAppStartProfiler (Lio/sentry/ITransactionProfiler;)V public fun setAppStartSamplingDecision (Lio/sentry/TracesSamplingDecision;)V public fun setAppStartType (Lio/sentry/android/core/performance/AppStartMetrics$AppStartType;)V diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java index 037b110c3d..0536dcabc7 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java @@ -4,12 +4,21 @@ import android.annotation.SuppressLint; import android.os.Build; +import io.sentry.CompositePerformanceCollector; import io.sentry.IContinuousProfiler; import io.sentry.ILogger; import io.sentry.IScopes; import io.sentry.ISentryExecutorService; +import io.sentry.NoOpScopes; +import io.sentry.PerformanceCollectionData; +import io.sentry.ProfileChunk; +import io.sentry.Sentry; import io.sentry.SentryLevel; +import io.sentry.SentryOptions; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; +import io.sentry.protocol.SentryId; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; import org.jetbrains.annotations.ApiStatus; @@ -23,7 +32,6 @@ public class AndroidContinuousProfiler implements IContinuousProfiler { private final @NotNull ILogger logger; private final @Nullable String profilingTracesDirPath; - private final boolean isProfilingEnabled; private final int profilingTracesHz; private final @NotNull ISentryExecutorService executorService; private final @NotNull BuildInfoProvider buildInfoProvider; @@ -32,21 +40,23 @@ public class AndroidContinuousProfiler implements IContinuousProfiler { private @Nullable AndroidProfiler profiler = null; private boolean isRunning = false; private @Nullable IScopes scopes; - private @Nullable Future closeFuture; + private @Nullable Future stopFuture; + private @Nullable CompositePerformanceCollector performanceCollector; + private final @NotNull List payloadBuilders = new ArrayList<>(); + private @NotNull SentryId profilerId = SentryId.EMPTY_ID; + private @NotNull SentryId chunkId = SentryId.EMPTY_ID; public AndroidContinuousProfiler( final @NotNull BuildInfoProvider buildInfoProvider, final @NotNull SentryFrameMetricsCollector frameMetricsCollector, final @NotNull ILogger logger, final @Nullable String profilingTracesDirPath, - final boolean isProfilingEnabled, final int profilingTracesHz, final @NotNull ISentryExecutorService executorService) { this.logger = logger; this.frameMetricsCollector = frameMetricsCollector; this.buildInfoProvider = buildInfoProvider; this.profilingTracesDirPath = profilingTracesDirPath; - this.isProfilingEnabled = isProfilingEnabled; this.profilingTracesHz = profilingTracesHz; this.executorService = executorService; } @@ -57,10 +67,6 @@ private void init() { return; } isInitialized = true; - if (!isProfilingEnabled) { - logger.log(SentryLevel.INFO, "Profiling is disabled in options."); - return; - } if (profilingTracesDirPath == null) { logger.log( SentryLevel.WARNING, @@ -81,15 +87,17 @@ private void init() { (int) SECONDS.toMicros(1) / profilingTracesHz, frameMetricsCollector, null, - logger, - buildInfoProvider); - } - - public synchronized void setScopes(final @NotNull IScopes scopes) { - this.scopes = scopes; + logger); } public synchronized void start() { + if ((scopes == null || scopes != NoOpScopes.getInstance()) + && Sentry.getCurrentScopes() != NoOpScopes.getInstance()) { + this.scopes = Sentry.getCurrentScopes(); + this.performanceCollector = + Sentry.getCurrentScopes().getOptions().getCompositePerformanceCollector(); + } + // Debug.startMethodTracingSampling() is only available since Lollipop, but Android Profiler // causes crashes on api 21 -> https://github.com/getsentry/sentry-java/issues/3392 if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) return; @@ -106,10 +114,23 @@ public synchronized void start() { if (startData == null) { return; } + isRunning = true; + if (profilerId == SentryId.EMPTY_ID) { + profilerId = new SentryId(); + } + + if (chunkId == SentryId.EMPTY_ID) { + chunkId = new SentryId(); + } + + if (performanceCollector != null) { + performanceCollector.start(chunkId.toString()); + } + try { - closeFuture = executorService.schedule(() -> stop(true), MAX_CHUNK_DURATION_MILLIS); + stopFuture = executorService.schedule(() -> stop(true), MAX_CHUNK_DURATION_MILLIS); } catch (RejectedExecutionException e) { logger.log( SentryLevel.ERROR, @@ -124,8 +145,8 @@ public synchronized void stop() { @SuppressLint("NewApi") private synchronized void stop(final boolean restartProfiler) { - if (closeFuture != null) { - closeFuture.cancel(true); + if (stopFuture != null) { + stopFuture.cancel(true); } // check if profiler was created and it's running if (profiler == null || !isRunning) { @@ -138,22 +159,44 @@ private synchronized void stop(final boolean restartProfiler) { return; } - // todo add PerformanceCollectionData - final AndroidProfiler.ProfileEndData endData = profiler.endAndCollect(false, null); + List performanceCollectionData = null; + if (performanceCollector != null) { + performanceCollectionData = performanceCollector.stop(chunkId.toString()); + } + + final AndroidProfiler.ProfileEndData endData = + profiler.endAndCollect(false, performanceCollectionData); // check if profiler end successfully if (endData == null) { - return; + logger.log( + SentryLevel.ERROR, + "An error occurred while collecting a profile chunk, and it won't be sent."); + } else { + // The scopes can be null if the profiler is started before the SDK is initialized (app start + // profiling), meaning there's no scopes to send the chunks. In that case, we store the data + // in a list and send it when the next chunk is finished. + synchronized (payloadBuilders) { + payloadBuilders.add( + new ProfileChunk.Builder( + profilerId, chunkId, endData.measurementsMap, endData.traceFile)); + } } isRunning = false; + // A chunk is finished. Next chunk will have a different id. + chunkId = SentryId.EMPTY_ID; - // todo schedule capture profile chunk envelope + if (scopes != null) { + sendChunks(scopes, scopes.getOptions()); + } if (restartProfiler) { logger.log(SentryLevel.DEBUG, "Profile chunk finished. Starting a new one."); start(); } else { + // When the profiler is stopped manually, we have to reset its id + profilerId = SentryId.EMPTY_ID; logger.log(SentryLevel.DEBUG, "Profile chunk finished."); } } @@ -162,6 +205,33 @@ public synchronized void close() { stop(); } + @Override + public @NotNull SentryId getProfilerId() { + return profilerId; + } + + private void sendChunks(final @NotNull IScopes scopes, final @NotNull SentryOptions options) { + try { + options + .getExecutorService() + .submit( + () -> { + final ArrayList payloads = new ArrayList<>(payloadBuilders.size()); + synchronized (payloadBuilders) { + for (ProfileChunk.Builder builder : payloadBuilders) { + payloads.add(builder.build(options)); + } + payloadBuilders.clear(); + } + for (ProfileChunk payload : payloads) { + scopes.captureProfileChunk(payload); + } + }); + } catch (Throwable e) { + options.getLogger().log(SentryLevel.DEBUG, "Failed to send profile chunks.", e); + } + } + @Override public boolean isRunning() { return isRunning; @@ -169,7 +239,7 @@ public boolean isRunning() { @VisibleForTesting @Nullable - Future getCloseFuture() { - return closeFuture; + Future getStopFuture() { + return stopFuture; } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidCpuCollector.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidCpuCollector.java index ccd878761d..800d4826fe 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidCpuCollector.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidCpuCollector.java @@ -8,6 +8,7 @@ import io.sentry.IPerformanceSnapshotCollector; import io.sentry.PerformanceCollectionData; import io.sentry.SentryLevel; +import io.sentry.SentryNanotimeDate; import io.sentry.util.FileUtils; import io.sentry.util.Objects; import java.io.File; @@ -73,7 +74,7 @@ public void collect(final @NotNull PerformanceCollectionData performanceCollecti CpuCollectionData cpuData = new CpuCollectionData( - System.currentTimeMillis(), (cpuUsagePercentage / (double) numCores) * 100.0); + (cpuUsagePercentage / (double) numCores) * 100.0, new SentryNanotimeDate()); performanceCollectionData.addCpuData(cpuData); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMemoryCollector.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMemoryCollector.java index f475c1801b..41c2d2da03 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMemoryCollector.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMemoryCollector.java @@ -4,6 +4,7 @@ import io.sentry.IPerformanceSnapshotCollector; import io.sentry.MemoryCollectionData; import io.sentry.PerformanceCollectionData; +import io.sentry.SentryNanotimeDate; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -15,10 +16,10 @@ public void setup() {} @Override public void collect(final @NotNull PerformanceCollectionData performanceCollectionData) { - long now = System.currentTimeMillis(); long usedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); long usedNativeMemory = Debug.getNativeHeapSize() - Debug.getNativeHeapFreeSize(); - MemoryCollectionData memoryData = new MemoryCollectionData(now, usedMemory, usedNativeMemory); + MemoryCollectionData memoryData = + new MemoryCollectionData(usedMemory, usedNativeMemory, new SentryNanotimeDate()); performanceCollectionData.addMemoryData(memoryData); } } 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 62900c25b8..605294f666 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 @@ -6,11 +6,14 @@ import android.content.Context; import android.content.pm.PackageInfo; import io.sentry.DeduplicateMultithreadedEventProcessor; -import io.sentry.DefaultTransactionPerformanceCollector; +import io.sentry.DefaultCompositePerformanceCollector; +import io.sentry.IContinuousProfiler; import io.sentry.ILogger; import io.sentry.ISentryLifecycleToken; import io.sentry.ITransactionProfiler; import io.sentry.NoOpConnectionStatusProvider; +import io.sentry.NoOpContinuousProfiler; +import io.sentry.NoOpTransactionProfiler; import io.sentry.ScopeType; import io.sentry.SendFireAndForgetEnvelopeSender; import io.sentry.SendFireAndForgetOutboxSender; @@ -159,23 +162,23 @@ static void initializeIntegrationsAndProcessors( // Check if the profiler was already instantiated in the app start. // We use the Android profiler, that uses a global start/stop api, so we need to preserve the // state of the profiler, and it's only possible retaining the instance. + final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance(); + final @Nullable ITransactionProfiler appStartTransactionProfiler; + final @Nullable IContinuousProfiler appStartContinuousProfiler; try (final @NotNull ISentryLifecycleToken ignored = AppStartMetrics.staticLock.acquire()) { - final @Nullable ITransactionProfiler appStartProfiler = - AppStartMetrics.getInstance().getAppStartProfiler(); - if (appStartProfiler != null) { - options.setTransactionProfiler(appStartProfiler); - AppStartMetrics.getInstance().setAppStartProfiler(null); - } else { - options.setTransactionProfiler( - new AndroidTransactionProfiler( - context, - options, - buildInfoProvider, - Objects.requireNonNull( - options.getFrameMetricsCollector(), - "options.getFrameMetricsCollector is required"))); - } + appStartTransactionProfiler = appStartMetrics.getAppStartProfiler(); + appStartContinuousProfiler = appStartMetrics.getAppStartContinuousProfiler(); + appStartMetrics.setAppStartProfiler(null); + appStartMetrics.setAppStartContinuousProfiler(null); } + + setupProfiler( + options, + context, + buildInfoProvider, + appStartTransactionProfiler, + appStartContinuousProfiler); + options.setModulesLoader(new AssetsModulesLoader(context, options.getLogger())); options.setDebugMetaLoader(new AssetsDebugMetaLoader(context, options.getLogger())); @@ -223,7 +226,7 @@ static void initializeIntegrationsAndProcessors( "options.getFrameMetricsCollector is required"))); } } - options.setTransactionPerformanceCollector(new DefaultTransactionPerformanceCollector(options)); + options.setCompositePerformanceCollector(new DefaultCompositePerformanceCollector(options)); if (options.getCacheDirPath() != null) { if (options.isEnableScopePersistence()) { @@ -233,6 +236,56 @@ static void initializeIntegrationsAndProcessors( } } + /** Setup the correct profiler (transaction or continuous) based on the options. */ + private static void setupProfiler( + final @NotNull SentryAndroidOptions options, + final @NotNull Context context, + final @NotNull BuildInfoProvider buildInfoProvider, + final @Nullable ITransactionProfiler appStartTransactionProfiler, + final @Nullable IContinuousProfiler appStartContinuousProfiler) { + if (options.isProfilingEnabled() || options.getProfilesSampleRate() != null) { + options.setContinuousProfiler(NoOpContinuousProfiler.getInstance()); + // This is a safeguard, but it should never happen, as the app start profiler should be the + // continuous one. + if (appStartContinuousProfiler != null) { + appStartContinuousProfiler.close(); + } + if (appStartTransactionProfiler != null) { + options.setTransactionProfiler(appStartTransactionProfiler); + } else { + options.setTransactionProfiler( + new AndroidTransactionProfiler( + context, + options, + buildInfoProvider, + Objects.requireNonNull( + options.getFrameMetricsCollector(), + "options.getFrameMetricsCollector is required"))); + } + } else { + options.setTransactionProfiler(NoOpTransactionProfiler.getInstance()); + // This is a safeguard, but it should never happen, as the app start profiler should be the + // transaction one. + if (appStartTransactionProfiler != null) { + appStartTransactionProfiler.close(); + } + if (appStartContinuousProfiler != null) { + options.setContinuousProfiler(appStartContinuousProfiler); + } else { + options.setContinuousProfiler( + new AndroidContinuousProfiler( + buildInfoProvider, + Objects.requireNonNull( + options.getFrameMetricsCollector(), + "options.getFrameMetricsCollector is required"), + options.getLogger(), + options.getProfilingTracesDirPath(), + options.getProfilingTracesHz(), + options.getExecutorService())); + } + } + } + static void installDefaultIntegrations( final @NotNull Context context, final @NotNull SentryAndroidOptions options, diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java index 0b31c8a206..e51e06fc6e 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java @@ -11,7 +11,9 @@ import io.sentry.ISentryLifecycleToken; import io.sentry.MemoryCollectionData; import io.sentry.PerformanceCollectionData; +import io.sentry.SentryDate; import io.sentry.SentryLevel; +import io.sentry.SentryNanotimeDate; import io.sentry.SentryUUID; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.profilemeasurements.ProfileMeasurement; @@ -154,6 +156,7 @@ public void onFrameMetricCollected( // profileStartNanos is calculated through SystemClock.elapsedRealtimeNanos(), // but frameEndNanos uses System.nanotime(), so we convert it to get the timestamp // relative to profileStartNanos + final SentryDate timestamp = new SentryNanotimeDate(); final long frameTimestampRelativeNanos = frameEndNanos - System.nanoTime() @@ -167,15 +170,18 @@ public void onFrameMetricCollected( } if (isFrozen) { frozenFrameRenderMeasurements.addLast( - new ProfileMeasurementValue(frameTimestampRelativeNanos, durationNanos)); + new ProfileMeasurementValue( + frameTimestampRelativeNanos, durationNanos, timestamp)); } else if (isSlow) { slowFrameRenderMeasurements.addLast( - new ProfileMeasurementValue(frameTimestampRelativeNanos, durationNanos)); + new ProfileMeasurementValue( + frameTimestampRelativeNanos, durationNanos, timestamp)); } if (refreshRate != lastRefreshRate) { lastRefreshRate = refreshRate; screenFrameRateMeasurements.addLast( - new ProfileMeasurementValue(frameTimestampRelativeNanos, refreshRate)); + new ProfileMeasurementValue( + frameTimestampRelativeNanos, refreshRate, timestamp)); } } }); @@ -318,20 +324,23 @@ private void putPerformanceCollectionDataInMeasurements( if (cpuData != null) { cpuUsageMeasurements.add( new ProfileMeasurementValue( - TimeUnit.MILLISECONDS.toNanos(cpuData.getTimestampMillis()) + timestampDiff, - cpuData.getCpuUsagePercentage())); + cpuData.getTimestamp().nanoTimestamp() + timestampDiff, + cpuData.getCpuUsagePercentage(), + cpuData.getTimestamp())); } if (memoryData != null && memoryData.getUsedHeapMemory() > -1) { memoryUsageMeasurements.add( new ProfileMeasurementValue( - TimeUnit.MILLISECONDS.toNanos(memoryData.getTimestampMillis()) + timestampDiff, - memoryData.getUsedHeapMemory())); + memoryData.getTimestamp().nanoTimestamp() + timestampDiff, + memoryData.getUsedHeapMemory(), + memoryData.getTimestamp())); } if (memoryData != null && memoryData.getUsedNativeMemory() > -1) { nativeMemoryUsageMeasurements.add( new ProfileMeasurementValue( - TimeUnit.MILLISECONDS.toNanos(memoryData.getTimestampMillis()) + timestampDiff, - memoryData.getUsedNativeMemory())); + memoryData.getTimestamp().nanoTimestamp() + timestampDiff, + memoryData.getUsedNativeMemory(), + memoryData.getTimestamp())); } } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java index 64a1ceda60..1d76775b3f 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryPerformanceProvider.java @@ -12,6 +12,7 @@ import android.os.Process; import android.os.SystemClock; import androidx.annotation.NonNull; +import io.sentry.IContinuousProfiler; import io.sentry.ILogger; import io.sentry.ISentryLifecycleToken; import io.sentry.ITransactionProfiler; @@ -100,6 +101,11 @@ public void shutdown() { if (appStartProfiler != null) { appStartProfiler.close(); } + final @Nullable IContinuousProfiler appStartContinuousProfiler = + AppStartMetrics.getInstance().getAppStartContinuousProfiler(); + if (appStartContinuousProfiler != null) { + appStartContinuousProfiler.close(); + } } } @@ -132,40 +138,18 @@ private void launchAppStartProfiler(final @NotNull AppStartMetrics appStartMetri return; } + if (profilingOptions.isContinuousProfilingEnabled()) { + createAndStartContinuousProfiler(context, profilingOptions, appStartMetrics); + return; + } + if (!profilingOptions.isProfilingEnabled()) { logger.log( SentryLevel.INFO, "Profiling is not enabled. App start profiling will not start."); return; } - final @NotNull TracesSamplingDecision appStartSamplingDecision = - new TracesSamplingDecision( - profilingOptions.isTraceSampled(), - profilingOptions.getTraceSampleRate(), - profilingOptions.isProfileSampled(), - profilingOptions.getProfileSampleRate()); - // We store any sampling decision, so we can respect it when the first transaction starts - appStartMetrics.setAppStartSamplingDecision(appStartSamplingDecision); - - if (!(appStartSamplingDecision.getProfileSampled() - && appStartSamplingDecision.getSampled())) { - logger.log(SentryLevel.DEBUG, "App start profiling was not sampled. It will not start."); - return; - } - logger.log(SentryLevel.DEBUG, "App start profiling started."); - - final @NotNull ITransactionProfiler appStartProfiler = - new AndroidTransactionProfiler( - context, - buildInfoProvider, - new SentryFrameMetricsCollector(context, logger, buildInfoProvider), - logger, - profilingOptions.getProfilingTracesDirPath(), - profilingOptions.isProfilingEnabled(), - profilingOptions.getProfilingTracesHz(), - new SentryExecutorService()); - appStartMetrics.setAppStartProfiler(appStartProfiler); - appStartProfiler.start(); + createAndStartTransactionProfiler(context, profilingOptions, appStartMetrics); } catch (FileNotFoundException e) { logger.log(SentryLevel.ERROR, "App start profiling config file not found. ", e); @@ -174,6 +158,59 @@ private void launchAppStartProfiler(final @NotNull AppStartMetrics appStartMetri } } + private void createAndStartContinuousProfiler( + final @NotNull Context context, + final @NotNull SentryAppStartProfilingOptions profilingOptions, + final @NotNull AppStartMetrics appStartMetrics) { + final @NotNull IContinuousProfiler appStartContinuousProfiler = + new AndroidContinuousProfiler( + buildInfoProvider, + new SentryFrameMetricsCollector( + context.getApplicationContext(), logger, buildInfoProvider), + logger, + profilingOptions.getProfilingTracesDirPath(), + profilingOptions.getProfilingTracesHz(), + new SentryExecutorService()); + appStartMetrics.setAppStartProfiler(null); + appStartMetrics.setAppStartContinuousProfiler(appStartContinuousProfiler); + logger.log(SentryLevel.DEBUG, "App start continuous profiling started."); + appStartContinuousProfiler.start(); + } + + private void createAndStartTransactionProfiler( + final @NotNull Context context, + final @NotNull SentryAppStartProfilingOptions profilingOptions, + final @NotNull AppStartMetrics appStartMetrics) { + final @NotNull TracesSamplingDecision appStartSamplingDecision = + new TracesSamplingDecision( + profilingOptions.isTraceSampled(), + profilingOptions.getTraceSampleRate(), + profilingOptions.isProfileSampled(), + profilingOptions.getProfileSampleRate()); + // We store any sampling decision, so we can respect it when the first transaction starts + appStartMetrics.setAppStartSamplingDecision(appStartSamplingDecision); + + if (!(appStartSamplingDecision.getProfileSampled() && appStartSamplingDecision.getSampled())) { + logger.log(SentryLevel.DEBUG, "App start profiling was not sampled. It will not start."); + return; + } + + final @NotNull ITransactionProfiler appStartProfiler = + new AndroidTransactionProfiler( + context, + buildInfoProvider, + new SentryFrameMetricsCollector(context, logger, buildInfoProvider), + logger, + profilingOptions.getProfilingTracesDirPath(), + profilingOptions.isProfilingEnabled(), + profilingOptions.getProfilingTracesHz(), + new SentryExecutorService()); + appStartMetrics.setAppStartContinuousProfiler(null); + appStartMetrics.setAppStartProfiler(appStartProfiler); + logger.log(SentryLevel.DEBUG, "App start profiling started."); + appStartProfiler.start(); + } + @SuppressLint("NewApi") private void onAppLaunched( final @Nullable Context context, final @NotNull AppStartMetrics appStartMetrics) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidThreadChecker.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidThreadChecker.java index 15781d711f..ccd4a92b27 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidThreadChecker.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/AndroidThreadChecker.java @@ -39,6 +39,11 @@ public boolean isMainThread() { return isMainThread(Thread.currentThread()); } + @Override + public @NotNull String getCurrentThreadName() { + return isMainThread() ? "main" : Thread.currentThread().getName(); + } + @Override public boolean isMainThread(final @NotNull SentryThread sentryThread) { final Long threadId = sentryThread.getId(); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java index 996c6ab171..f43b44a5ad 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/performance/AppStartMetrics.java @@ -10,6 +10,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import io.sentry.IContinuousProfiler; import io.sentry.ISentryLifecycleToken; import io.sentry.ITransactionProfiler; import io.sentry.SentryDate; @@ -57,6 +58,7 @@ public enum AppStartType { private final @NotNull Map contentProviderOnCreates; private final @NotNull List activityLifecycles; private @Nullable ITransactionProfiler appStartProfiler = null; + private @Nullable IContinuousProfiler appStartContinuousProfiler = null; private @Nullable TracesSamplingDecision appStartSamplingDecision = null; private @Nullable SentryDate onCreateTime = null; private boolean appLaunchTooLong = false; @@ -186,6 +188,10 @@ public void clear() { appStartProfiler.close(); } appStartProfiler = null; + if (appStartContinuousProfiler != null) { + appStartContinuousProfiler.close(); + } + appStartContinuousProfiler = null; appStartSamplingDecision = null; appLaunchTooLong = false; appLaunchedInForeground = false; @@ -201,6 +207,15 @@ public void setAppStartProfiler(final @Nullable ITransactionProfiler appStartPro this.appStartProfiler = appStartProfiler; } + public @Nullable IContinuousProfiler getAppStartContinuousProfiler() { + return appStartContinuousProfiler; + } + + public void setAppStartContinuousProfiler( + final @Nullable IContinuousProfiler appStartContinuousProfiler) { + this.appStartContinuousProfiler = appStartContinuousProfiler; + } + public void setAppStartSamplingDecision( final @Nullable TracesSamplingDecision appStartSamplingDecision) { this.appStartSamplingDecision = appStartSamplingDecision; @@ -259,11 +274,15 @@ private void checkCreateTimeOnMain(final @NotNull Application application) { if (onCreateTime == null) { appLaunchedInForeground = false; - // we stop the app start profiler, as it's useless and likely to timeout + // we stop the app start profilers, as they are useless and likely to timeout if (appStartProfiler != null && appStartProfiler.isRunning()) { appStartProfiler.close(); appStartProfiler = null; } + if (appStartContinuousProfiler != null && appStartContinuousProfiler.isRunning()) { + appStartContinuousProfiler.close(); + appStartContinuousProfiler = null; + } } application.unregisterActivityLifecycleCallbacks(instance); }); diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidContinuousProfilerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidContinuousProfilerTest.kt index 3efc1f0cf5..70ed75186c 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidContinuousProfilerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidContinuousProfilerTest.kt @@ -4,17 +4,25 @@ import android.content.Context import android.os.Build import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.sentry.IHub +import io.sentry.CompositePerformanceCollector +import io.sentry.CpuCollectionData import io.sentry.ILogger +import io.sentry.IScopes import io.sentry.ISentryExecutorService +import io.sentry.MemoryCollectionData +import io.sentry.PerformanceCollectionData +import io.sentry.Sentry import io.sentry.SentryLevel +import io.sentry.SentryNanotimeDate import io.sentry.SentryTracer import io.sentry.TransactionContext import io.sentry.android.core.internal.util.SentryFrameMetricsCollector +import io.sentry.profilemeasurements.ProfileMeasurement import io.sentry.test.DeferredExecutorService import io.sentry.test.getProperty import org.junit.runner.RunWith import org.mockito.kotlin.any +import org.mockito.kotlin.check import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never @@ -28,6 +36,7 @@ import java.util.concurrent.Future import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test +import kotlin.test.assertContains import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertNull @@ -45,7 +54,7 @@ class AndroidContinuousProfilerTest { } val mockLogger = mock() - val hub: IHub = mock() + val scopes: IScopes = mock() val frameMetricsCollector: SentryFrameMetricsCollector = mock() lateinit var transaction1: SentryTracer @@ -61,16 +70,15 @@ class AndroidContinuousProfilerTest { fun getSut(buildInfoProvider: BuildInfoProvider = buildInfo, optionConfig: ((options: SentryAndroidOptions) -> Unit) = {}): AndroidContinuousProfiler { optionConfig(options) - whenever(hub.options).thenReturn(options) - transaction1 = SentryTracer(TransactionContext("", ""), hub) - transaction2 = SentryTracer(TransactionContext("", ""), hub) - transaction3 = SentryTracer(TransactionContext("", ""), hub) + whenever(scopes.options).thenReturn(options) + transaction1 = SentryTracer(TransactionContext("", ""), scopes) + transaction2 = SentryTracer(TransactionContext("", ""), scopes) + transaction3 = SentryTracer(TransactionContext("", ""), scopes) return AndroidContinuousProfiler( buildInfoProvider, frameMetricsCollector, options.logger, options.profilingTracesDirPath, - options.isProfilingEnabled, options.profilingTracesHz, options.executorService ) @@ -111,6 +119,8 @@ class AndroidContinuousProfilerTest { // Profiler doesn't start if the folder doesn't exists. // Usually it's generated when calling Sentry.init, but for tests we can create it manually. File(fixture.options.profilingTracesDirPath!!).mkdirs() + + Sentry.setCurrentScopes(fixture.scopes) } @AfterTest @@ -149,26 +159,12 @@ class AndroidContinuousProfilerTest { } @Test - fun `profiler on profilesSampleRate=0 false`() { - val profiler = fixture.getSut { - it.profilesSampleRate = 0.0 - } - profiler.start() - assertFalse(profiler.isRunning) - } - - @Test - fun `profiler evaluates if profiling is enabled in options only on first start`() { - // We create the profiler, and nothing goes wrong + fun `profiler ignores profilesSampleRate`() { val profiler = fixture.getSut { it.profilesSampleRate = 0.0 } - verify(fixture.mockLogger, never()).log(SentryLevel.INFO, "Profiling is disabled in options.") - - // Regardless of how many times the profiler is started, the option is evaluated and logged only once profiler.start() - profiler.start() - verify(fixture.mockLogger, times(1)).log(SentryLevel.INFO, "Profiling is disabled in options.") + assertTrue(profiler.isRunning) } @Test @@ -269,6 +265,27 @@ class AndroidContinuousProfilerTest { verify(fixture.mockLogger).log(eq(SentryLevel.ERROR), eq("Error while stopping profiling: "), any()) } + @Test + fun `profiler starts performance collector on start`() { + val performanceCollector = mock() + fixture.options.compositePerformanceCollector = performanceCollector + val profiler = fixture.getSut() + verify(performanceCollector, never()).start(any()) + profiler.start() + verify(performanceCollector).start(any()) + } + + @Test + fun `profiler stops performance collector on stop`() { + val performanceCollector = mock() + fixture.options.compositePerformanceCollector = performanceCollector + val profiler = fixture.getSut() + profiler.start() + verify(performanceCollector, never()).stop(any()) + profiler.stop() + verify(performanceCollector).stop(any()) + } + @Test fun `profiler stops collecting frame metrics when it stops`() { val profiler = fixture.getSut() @@ -294,9 +311,9 @@ class AndroidContinuousProfilerTest { val scheduledJob = androidProfiler?.getProperty?>("scheduledFinish") assertNull(scheduledJob) - val closeFuture = profiler.closeFuture - assertNotNull(closeFuture) - assertTrue(closeFuture.isCancelled) + val stopFuture = profiler.stopFuture + assertNotNull(stopFuture) + assertTrue(stopFuture.isCancelled) } @Test @@ -316,4 +333,65 @@ class AndroidContinuousProfilerTest { verify(fixture.mockLogger, times(2)).log(eq(SentryLevel.DEBUG), eq("Profile chunk finished. Starting a new one.")) assertTrue(profiler.isRunning) } + + @Test + fun `profiler sends chunk on each restart`() { + val executorService = DeferredExecutorService() + val profiler = fixture.getSut { + it.executorService = executorService + } + profiler.start() + assertTrue(profiler.isRunning) + // We run the executor service to trigger the profiler restart (chunk finish) + executorService.runAll() + verify(fixture.scopes, never()).captureProfileChunk(any()) + // Now the executor is used to send the chunk + executorService.runAll() + verify(fixture.scopes).captureProfileChunk(any()) + } + + @Test + fun `profiler sends chunk with measurements`() { + val executorService = DeferredExecutorService() + val performanceCollector = mock() + val collectionData = PerformanceCollectionData() + + collectionData.addMemoryData(MemoryCollectionData(2, 3, SentryNanotimeDate())) + collectionData.addCpuData(CpuCollectionData(3.0, SentryNanotimeDate())) + whenever(performanceCollector.stop(any())).thenReturn(listOf(collectionData)) + + fixture.options.compositePerformanceCollector = performanceCollector + val profiler = fixture.getSut { + it.executorService = executorService + } + profiler.start() + profiler.stop() + // We run the executor service to send the profile chunk + executorService.runAll() + verify(fixture.scopes).captureProfileChunk( + check { + assertContains(it.measurements, ProfileMeasurement.ID_CPU_USAGE) + assertContains(it.measurements, ProfileMeasurement.ID_MEMORY_FOOTPRINT) + assertContains(it.measurements, ProfileMeasurement.ID_MEMORY_NATIVE_FOOTPRINT) + } + ) + } + + @Test + fun `profiler sends another chunk on stop`() { + val executorService = DeferredExecutorService() + val profiler = fixture.getSut { + it.executorService = executorService + } + profiler.start() + assertTrue(profiler.isRunning) + // We run the executor service to trigger the profiler restart (chunk finish) + executorService.runAll() + verify(fixture.scopes, never()).captureProfileChunk(any()) + // We stop the profiler, which should send an additional chunk + profiler.stop() + // Now the executor is used to send the chunk + executorService.runAll() + verify(fixture.scopes, times(2)).captureProfileChunk(any()) + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidCpuCollectorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidCpuCollectorTest.kt index 7f54b2c353..f86415037a 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidCpuCollectorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidCpuCollectorTest.kt @@ -45,6 +45,6 @@ class AndroidCpuCollectorTest { val cpuData = data.cpuData assertNotNull(cpuData) assertNotEquals(0.0, cpuData.cpuUsagePercentage) - assertNotEquals(0, cpuData.timestampMillis) + assertNotEquals(0, cpuData.timestamp.nanoTimestamp()) } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidMemoryCollectorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidMemoryCollectorTest.kt index 7879c2daf5..be41874a83 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidMemoryCollectorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidMemoryCollectorTest.kt @@ -27,6 +27,6 @@ class AndroidMemoryCollectorTest { assertNotEquals(-1, memoryData.usedNativeMemory) assertEquals(usedNativeMemory, memoryData.usedNativeMemory) assertEquals(usedMemory, memoryData.usedHeapMemory) - assertNotEquals(0, memoryData.timestampMillis) + assertNotEquals(0, memoryData.timestamp.nanoTimestamp()) } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index 95ca59a06e..56571b4431 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -6,14 +6,19 @@ import android.os.Build import android.os.Bundle import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.sentry.DefaultTransactionPerformanceCollector +import io.sentry.DefaultCompositePerformanceCollector +import io.sentry.IContinuousProfiler import io.sentry.ILogger +import io.sentry.ITransactionProfiler import io.sentry.MainEventProcessor +import io.sentry.NoOpContinuousProfiler +import io.sentry.NoOpTransactionProfiler import io.sentry.SentryOptions import io.sentry.android.core.cache.AndroidEnvelopeCache import io.sentry.android.core.internal.gestures.AndroidViewGestureTargetLocator import io.sentry.android.core.internal.modules.AssetsModulesLoader import io.sentry.android.core.internal.util.AndroidThreadChecker +import io.sentry.android.core.performance.AppStartMetrics import io.sentry.android.fragment.FragmentLifecycleIntegration import io.sentry.android.replay.ReplayIntegration import io.sentry.android.timber.SentryTimberIntegration @@ -35,6 +40,7 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertIs +import kotlin.test.assertNotEquals import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue @@ -344,11 +350,113 @@ class AndroidOptionsInitializerTest { } @Test - fun `init should set Android transaction profiler`() { + fun `init should set Android continuous profiler`() { fixture.initSut() + assertNotNull(fixture.sentryOptions.transactionProfiler) + assertEquals(fixture.sentryOptions.transactionProfiler, NoOpTransactionProfiler.getInstance()) + assertTrue(fixture.sentryOptions.continuousProfiler is AndroidContinuousProfiler) + } + + @Test + fun `init with profilesSampleRate should set Android transaction profiler`() { + fixture.initSut(configureOptions = { + profilesSampleRate = 1.0 + }) + + assertNotNull(fixture.sentryOptions.transactionProfiler) + assertTrue(fixture.sentryOptions.transactionProfiler is AndroidTransactionProfiler) + assertEquals(fixture.sentryOptions.continuousProfiler, NoOpContinuousProfiler.getInstance()) + } + + @Test + fun `init with profilesSampleRate 0 should set Android transaction profiler`() { + fixture.initSut(configureOptions = { + profilesSampleRate = 0.0 + }) + assertNotNull(fixture.sentryOptions.transactionProfiler) assertTrue(fixture.sentryOptions.transactionProfiler is AndroidTransactionProfiler) + assertEquals(fixture.sentryOptions.continuousProfiler, NoOpContinuousProfiler.getInstance()) + } + + @Test + fun `init with profilesSampler should set Android transaction profiler`() { + fixture.initSut(configureOptions = { + profilesSampler = mock() + }) + + assertNotNull(fixture.sentryOptions.transactionProfiler) + assertTrue(fixture.sentryOptions.transactionProfiler is AndroidTransactionProfiler) + assertEquals(fixture.sentryOptions.continuousProfiler, NoOpContinuousProfiler.getInstance()) + } + + @Test + fun `init reuses transaction profiler of appStartMetrics, if exists`() { + val appStartProfiler = mock() + AppStartMetrics.getInstance().appStartProfiler = appStartProfiler + fixture.initSut(configureOptions = { + profilesSampler = mock() + }) + + assertEquals(appStartProfiler, fixture.sentryOptions.transactionProfiler) + assertEquals(fixture.sentryOptions.continuousProfiler, NoOpContinuousProfiler.getInstance()) + + // AppStartMetrics should be cleared + assertNull(AppStartMetrics.getInstance().appStartProfiler) + assertNull(AppStartMetrics.getInstance().appStartContinuousProfiler) + } + + @Test + fun `init reuses continuous profiler of appStartMetrics, if exists`() { + val appStartContinuousProfiler = mock() + AppStartMetrics.getInstance().appStartContinuousProfiler = appStartContinuousProfiler + fixture.initSut() + + assertEquals(fixture.sentryOptions.transactionProfiler, NoOpTransactionProfiler.getInstance()) + assertEquals(appStartContinuousProfiler, fixture.sentryOptions.continuousProfiler) + + // AppStartMetrics should be cleared + assertNull(AppStartMetrics.getInstance().appStartProfiler) + assertNull(AppStartMetrics.getInstance().appStartContinuousProfiler) + } + + @Test + fun `init with transaction profiling closes continuous profiler of appStartMetrics`() { + val appStartContinuousProfiler = mock() + AppStartMetrics.getInstance().appStartContinuousProfiler = appStartContinuousProfiler + fixture.initSut(configureOptions = { + profilesSampler = mock() + }) + + assertNotNull(fixture.sentryOptions.transactionProfiler) + assertNotEquals(NoOpTransactionProfiler.getInstance(), fixture.sentryOptions.transactionProfiler) + assertEquals(fixture.sentryOptions.continuousProfiler, NoOpContinuousProfiler.getInstance()) + + // app start profiler is closed, because it will never be used + verify(appStartContinuousProfiler).close() + + // AppStartMetrics should be cleared + assertNull(AppStartMetrics.getInstance().appStartProfiler) + assertNull(AppStartMetrics.getInstance().appStartContinuousProfiler) + } + + @Test + fun `init with continuous profiling closes transaction profiler of appStartMetrics`() { + val appStartProfiler = mock() + AppStartMetrics.getInstance().appStartProfiler = appStartProfiler + fixture.initSut() + + assertEquals(NoOpTransactionProfiler.getInstance(), fixture.sentryOptions.transactionProfiler) + assertNotNull(fixture.sentryOptions.continuousProfiler) + assertNotEquals(NoOpContinuousProfiler.getInstance(), fixture.sentryOptions.continuousProfiler) + + // app start profiler is closed, because it will never be used + verify(appStartProfiler).close() + + // AppStartMetrics should be cleared + assertNull(AppStartMetrics.getInstance().appStartProfiler) + assertNull(AppStartMetrics.getInstance().appStartContinuousProfiler) } @Test @@ -663,10 +771,10 @@ class AndroidOptionsInitializerTest { } @Test - fun `DefaultTransactionPerformanceCollector is set to options`() { + fun `DefaultCompositePerformanceCollector is set to options`() { fixture.initSut() - assertIs(fixture.sentryOptions.transactionPerformanceCollector) + assertIs(fixture.sentryOptions.compositePerformanceCollector) } @Test diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt index 670729c4d7..86e0d90001 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt @@ -8,6 +8,7 @@ import io.sentry.ILogger import io.sentry.ISentryExecutorService import io.sentry.MemoryCollectionData import io.sentry.PerformanceCollectionData +import io.sentry.SentryDate import io.sentry.SentryExecutorService import io.sentry.SentryLevel import io.sentry.android.core.internal.util.SentryFrameMetricsCollector @@ -258,12 +259,14 @@ class AndroidProfilerTest { val profiler = fixture.getSut() val performanceCollectionData = ArrayList() var singleData = PerformanceCollectionData() - singleData.addMemoryData(MemoryCollectionData(1, 2, 3)) - singleData.addCpuData(CpuCollectionData(1, 1.4)) + val t1 = mock() + val t2 = mock() + singleData.addMemoryData(MemoryCollectionData(2, 3, t1)) + singleData.addCpuData(CpuCollectionData(1.4, t1)) performanceCollectionData.add(singleData) singleData = PerformanceCollectionData() - singleData.addMemoryData(MemoryCollectionData(2, 3, 4)) + singleData.addMemoryData(MemoryCollectionData(3, 4, t2)) performanceCollectionData.add(singleData) profiler.start() diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt index b92db7cfc6..ec9e737559 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt @@ -460,12 +460,12 @@ class AndroidTransactionProfilerTest { val profiler = fixture.getSut(context) val performanceCollectionData = ArrayList() var singleData = PerformanceCollectionData() - singleData.addMemoryData(MemoryCollectionData(1, 2, 3)) - singleData.addCpuData(CpuCollectionData(1, 1.4)) + singleData.addMemoryData(MemoryCollectionData(2, 3, mock())) + singleData.addCpuData(CpuCollectionData(1.4, mock())) performanceCollectionData.add(singleData) singleData = PerformanceCollectionData() - singleData.addMemoryData(MemoryCollectionData(2, 3, 4)) + singleData.addMemoryData(MemoryCollectionData(3, 4, mock())) performanceCollectionData.add(singleData) profiler.start() diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt index 26a76af30e..237bc54867 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryPerformanceProviderTest.kt @@ -176,6 +176,7 @@ class SentryPerformanceProviderTest { fun `when config file does not exists, nothing happens`() { fixture.getSut() assertNull(AppStartMetrics.getInstance().appStartProfiler) + assertNull(AppStartMetrics.getInstance().appStartContinuousProfiler) verify(fixture.logger, never()).log(any(), any()) } @@ -186,6 +187,7 @@ class SentryPerformanceProviderTest { config.setReadable(false) } assertNull(AppStartMetrics.getInstance().appStartProfiler) + assertNull(AppStartMetrics.getInstance().appStartContinuousProfiler) verify(fixture.logger, never()).log(any(), any()) } @@ -195,6 +197,7 @@ class SentryPerformanceProviderTest { config.createNewFile() } assertNull(AppStartMetrics.getInstance().appStartProfiler) + assertNull(AppStartMetrics.getInstance().appStartContinuousProfiler) verify(fixture.logger).log( eq(SentryLevel.WARNING), eq("Unable to deserialize the SentryAppStartProfilingOptions. App start profiling will not start.") @@ -204,7 +207,7 @@ class SentryPerformanceProviderTest { @Test fun `when profiling is disabled, profiler is not started`() { fixture.getSut { config -> - writeConfig(config, profilingEnabled = false) + writeConfig(config, profilingEnabled = false, continuousProfilingEnabled = false) } assertNull(AppStartMetrics.getInstance().appStartProfiler) verify(fixture.logger).log( @@ -213,10 +216,22 @@ class SentryPerformanceProviderTest { ) } + @Test + fun `when continuous profiling is disabled, continuous profiler is not started`() { + fixture.getSut { config -> + writeConfig(config, continuousProfilingEnabled = false, profilingEnabled = false) + } + assertNull(AppStartMetrics.getInstance().appStartContinuousProfiler) + verify(fixture.logger).log( + eq(SentryLevel.INFO), + eq("Profiling is not enabled. App start profiling will not start.") + ) + } + @Test fun `when trace is not sampled, profiler is not started and sample decision is stored`() { fixture.getSut { config -> - writeConfig(config, traceSampled = false, profileSampled = true) + writeConfig(config, continuousProfilingEnabled = false, traceSampled = false, profileSampled = true) } assertNull(AppStartMetrics.getInstance().appStartProfiler) assertNotNull(AppStartMetrics.getInstance().appStartSamplingDecision) @@ -232,7 +247,7 @@ class SentryPerformanceProviderTest { @Test fun `when profile is not sampled, profiler is not started and sample decision is stored`() { fixture.getSut { config -> - writeConfig(config, traceSampled = true, profileSampled = false) + writeConfig(config, continuousProfilingEnabled = false, traceSampled = true, profileSampled = false) } assertNull(AppStartMetrics.getInstance().appStartProfiler) assertNotNull(AppStartMetrics.getInstance().appStartSamplingDecision) @@ -244,11 +259,26 @@ class SentryPerformanceProviderTest { ) } + // This case should never happen in reality, but it's technically possible to have such configuration @Test - fun `when profiler starts, it is set in AppStartMetrics`() { + fun `when both transaction and continuous profilers are enabled, only continuous profiler is created`() { fixture.getSut { config -> writeConfig(config) } + assertNull(AppStartMetrics.getInstance().appStartProfiler) + assertNotNull(AppStartMetrics.getInstance().appStartContinuousProfiler) + assertTrue(AppStartMetrics.getInstance().appStartContinuousProfiler!!.isRunning) + verify(fixture.logger).log( + eq(SentryLevel.DEBUG), + eq("App start continuous profiling started.") + ) + } + + @Test + fun `when profiler starts, it is set in AppStartMetrics`() { + fixture.getSut { config -> + writeConfig(config, continuousProfilingEnabled = false) + } assertNotNull(AppStartMetrics.getInstance().appStartProfiler) assertNotNull(AppStartMetrics.getInstance().appStartSamplingDecision) assertTrue(AppStartMetrics.getInstance().appStartProfiler!!.isRunning) @@ -260,19 +290,43 @@ class SentryPerformanceProviderTest { ) } + @Test + fun `when continuous profiler starts, it is set in AppStartMetrics`() { + fixture.getSut { config -> + writeConfig(config, profilingEnabled = false) + } + assertNotNull(AppStartMetrics.getInstance().appStartContinuousProfiler) + assertTrue(AppStartMetrics.getInstance().appStartContinuousProfiler!!.isRunning) + verify(fixture.logger).log( + eq(SentryLevel.DEBUG), + eq("App start continuous profiling started.") + ) + } + @Test fun `when provider is closed, profiler is stopped`() { val provider = fixture.getSut { config -> - writeConfig(config) + writeConfig(config, continuousProfilingEnabled = false) } provider.shutdown() assertNotNull(AppStartMetrics.getInstance().appStartProfiler) assertFalse(AppStartMetrics.getInstance().appStartProfiler!!.isRunning) } + @Test + fun `when provider is closed, continuous profiler is stopped`() { + val provider = fixture.getSut { config -> + writeConfig(config, profilingEnabled = false) + } + provider.shutdown() + assertNotNull(AppStartMetrics.getInstance().appStartContinuousProfiler) + assertFalse(AppStartMetrics.getInstance().appStartContinuousProfiler!!.isRunning) + } + private fun writeConfig( configFile: File, profilingEnabled: Boolean = true, + continuousProfilingEnabled: Boolean = true, traceSampled: Boolean = true, traceSampleRate: Double = 1.0, profileSampled: Boolean = true, @@ -281,6 +335,7 @@ class SentryPerformanceProviderTest { ) { val appStartProfilingOptions = SentryAppStartProfilingOptions() appStartProfilingOptions.isProfilingEnabled = profilingEnabled + appStartProfilingOptions.isContinuousProfilingEnabled = continuousProfilingEnabled appStartProfilingOptions.isTraceSampled = traceSampled appStartProfilingOptions.traceSampleRate = traceSampleRate appStartProfilingOptions.isProfileSampled = profileSampled 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 e392d238ab..eac9f4293f 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 @@ -10,6 +10,7 @@ import io.sentry.CheckIn import io.sentry.Hint import io.sentry.IScope import io.sentry.ISentryClient +import io.sentry.ProfileChunk import io.sentry.ProfilingTraceData import io.sentry.Sentry import io.sentry.SentryEnvelope @@ -176,6 +177,10 @@ class SessionTrackingIntegrationTest { TODO("Not yet implemented") } + override fun captureProfileChunk(profileChunk: ProfileChunk, scope: IScope?): SentryId { + TODO("Not yet implemented") + } + override fun captureCheckIn(checkIn: CheckIn, scope: IScope?, hint: Hint?): SentryId { TODO("Not yet implemented") } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidThreadCheckerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidThreadCheckerTest.kt index eb59f0732e..0b3729f8ce 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidThreadCheckerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/util/AndroidThreadCheckerTest.kt @@ -4,6 +4,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.protocol.SentryThread import org.junit.runner.RunWith import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -44,4 +45,23 @@ class AndroidThreadCheckerTest { } assertFalse(AndroidThreadChecker.getInstance().isMainThread(sentryThread)) } + + @Test + fun `currentThreadName returns main when called on the main thread`() { + val thread = Thread.currentThread() + thread.name = "test" + assertEquals("main", AndroidThreadChecker.getInstance().currentThreadName) + } + + @Test + fun `currentThreadName returns the name of the current thread`() { + var threadName = "" + val thread = Thread { + threadName = AndroidThreadChecker.getInstance().currentThreadName + } + thread.name = "test" + thread.start() + thread.join() + assertEquals("test", threadName) + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt index eb0e85dc28..8d3cf062b2 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/performance/AppStartMetricsTest.kt @@ -5,6 +5,7 @@ import android.content.ContentProvider import android.os.Build import android.os.Looper import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.sentry.IContinuousProfiler import io.sentry.ITransactionProfiler import io.sentry.android.core.SentryAndroidOptions import io.sentry.android.core.SentryShadowProcess @@ -56,7 +57,8 @@ class AppStartMetricsTest { metrics.addActivityLifecycleTimeSpans(ActivityLifecycleTimeSpan()) AppStartMetrics.onApplicationCreate(mock()) AppStartMetrics.onContentProviderCreate(mock()) - metrics.setAppStartProfiler(mock()) + metrics.appStartProfiler = mock() + metrics.appStartContinuousProfiler = mock() metrics.appStartSamplingDecision = mock() metrics.clear() @@ -69,6 +71,7 @@ class AppStartMetricsTest { assertTrue(metrics.activityLifecycleTimeSpans.isEmpty()) assertTrue(metrics.contentProviderOnCreateTimeSpans.isEmpty()) assertNull(metrics.appStartProfiler) + assertNull(metrics.appStartContinuousProfiler) assertNull(metrics.appStartSamplingDecision) } @@ -196,6 +199,19 @@ class AppStartMetricsTest { verify(profiler).close() } + @Test + fun `if activity is never started, stops app start continuous profiler if running`() { + val profiler = mock() + whenever(profiler.isRunning).thenReturn(true) + AppStartMetrics.getInstance().appStartContinuousProfiler = profiler + + AppStartMetrics.getInstance().registerApplicationForegroundCheck(mock()) + // Job on main thread checks if activity was launched + Shadows.shadowOf(Looper.getMainLooper()).idle() + + verify(profiler).close() + } + @Test fun `if activity is started, does not stop app start profiler if running`() { val profiler = mock() @@ -210,6 +226,20 @@ class AppStartMetricsTest { verify(profiler, never()).close() } + @Test + fun `if activity is started, does not stop app start continuous profiler if running`() { + val profiler = mock() + whenever(profiler.isRunning).thenReturn(true) + AppStartMetrics.getInstance().appStartContinuousProfiler = profiler + AppStartMetrics.getInstance().onActivityCreated(mock(), mock()) + + AppStartMetrics.getInstance().registerApplicationForegroundCheck(mock()) + // Job on main thread checks if activity was launched + Shadows.shadowOf(Looper.getMainLooper()).idle() + + verify(profiler, never()).close() + } + @Test fun `if app start span is longer than 1 minute, appStartTimeSpanWithFallback returns an empty span`() { val appStartTimeSpan = AppStartMetrics.getInstance().appStartTimeSpan diff --git a/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SQLiteSpanManagerTest.kt b/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SQLiteSpanManagerTest.kt index 17c37d69bf..b292f0d038 100644 --- a/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SQLiteSpanManagerTest.kt +++ b/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SQLiteSpanManagerTest.kt @@ -100,6 +100,7 @@ class SQLiteSpanManagerTest { fixture.options.threadChecker = mock() whenever(fixture.options.threadChecker.isMainThread).thenReturn(false) + whenever(fixture.options.threadChecker.currentThreadName).thenReturn("test") sut.performSql("sql") {} val span = fixture.sentryTracer.children.first() @@ -114,6 +115,7 @@ class SQLiteSpanManagerTest { fixture.options.threadChecker = mock() whenever(fixture.options.threadChecker.isMainThread).thenReturn(true) + whenever(fixture.options.threadChecker.currentThreadName).thenReturn("test") sut.performSql("sql") {} val span = fixture.sentryTracer.children.first() diff --git a/sentry-opentelemetry/sentry-opentelemetry-extra/api/sentry-opentelemetry-extra.api b/sentry-opentelemetry/sentry-opentelemetry-extra/api/sentry-opentelemetry-extra.api index bb749a7df1..80e3c55d1c 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-extra/api/sentry-opentelemetry-extra.api +++ b/sentry-opentelemetry/sentry-opentelemetry-extra/api/sentry-opentelemetry-extra.api @@ -21,7 +21,7 @@ public final class io/sentry/opentelemetry/OtelSpanContext : io/sentry/SpanConte public final class io/sentry/opentelemetry/OtelSpanFactory : io/sentry/ISpanFactory { public fun ()V public fun createSpan (Lio/sentry/IScopes;Lio/sentry/SpanOptions;Lio/sentry/SpanContext;Lio/sentry/ISpan;)Lio/sentry/ISpan; - public fun createTransaction (Lio/sentry/TransactionContext;Lio/sentry/IScopes;Lio/sentry/TransactionOptions;Lio/sentry/TransactionPerformanceCollector;)Lio/sentry/ITransaction; + public fun createTransaction (Lio/sentry/TransactionContext;Lio/sentry/IScopes;Lio/sentry/TransactionOptions;Lio/sentry/CompositePerformanceCollector;)Lio/sentry/ITransaction; } public final class io/sentry/opentelemetry/OtelSpanWrapper : io/sentry/ISpan { diff --git a/sentry-opentelemetry/sentry-opentelemetry-extra/src/main/java/io/sentry/opentelemetry/OtelSpanFactory.java b/sentry-opentelemetry/sentry-opentelemetry-extra/src/main/java/io/sentry/opentelemetry/OtelSpanFactory.java index 7869f6dc96..19de213e18 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-extra/src/main/java/io/sentry/opentelemetry/OtelSpanFactory.java +++ b/sentry-opentelemetry/sentry-opentelemetry-extra/src/main/java/io/sentry/opentelemetry/OtelSpanFactory.java @@ -8,6 +8,7 @@ import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.context.Context; import io.sentry.Baggage; +import io.sentry.CompositePerformanceCollector; import io.sentry.IScopes; import io.sentry.ISpan; import io.sentry.ISpanFactory; @@ -21,7 +22,6 @@ import io.sentry.TracesSamplingDecision; import io.sentry.TransactionContext; import io.sentry.TransactionOptions; -import io.sentry.TransactionPerformanceCollector; import io.sentry.protocol.SentryId; import io.sentry.util.SpanUtils; import java.util.concurrent.TimeUnit; @@ -39,7 +39,7 @@ public final class OtelSpanFactory implements ISpanFactory { @NotNull TransactionContext context, @NotNull IScopes scopes, @NotNull TransactionOptions transactionOptions, - @Nullable TransactionPerformanceCollector transactionPerformanceCollector) { + @Nullable CompositePerformanceCollector compositePerformanceCollector) { final @Nullable OtelSpanWrapper span = createSpanInternal( scopes, transactionOptions, null, context.getSamplingDecision(), context); diff --git a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml index ac85411017..fa070ac644 100644 --- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml +++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml @@ -110,7 +110,7 @@ - + diff --git a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java index a4a1c5397a..572c4cdba7 100644 --- a/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java +++ b/sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java @@ -2,12 +2,14 @@ import android.app.Application; import android.os.StrictMode; +import io.sentry.Sentry; /** Apps. main Application. */ public class MyApplication extends Application { @Override public void onCreate() { + Sentry.startProfiler(); strictMode(); super.onCreate(); diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebFluxTracingFilterTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebFluxTracingFilterTest.kt index 36a5ccf1e6..06b7a6f8d4 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebFluxTracingFilterTest.kt +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebFluxTracingFilterTest.kt @@ -248,7 +248,7 @@ class SentryWebFluxTracingFilterTest { verify(fixture.chain).filter(fixture.exchange) verify(fixture.scopes, times(2)).isEnabled - verify(fixture.scopes, times(2)).options + verify(fixture.scopes, times(3)).options verify(fixture.scopes).continueTrace(anyOrNull(), anyOrNull()) verify(fixture.scopes).addBreadcrumb(any(), any()) verify(fixture.scopes).configureScope(any()) diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryWebFluxTracingFilterTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryWebFluxTracingFilterTest.kt index 67b2c021f8..7711541cef 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryWebFluxTracingFilterTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/webflux/SentryWebFluxTracingFilterTest.kt @@ -249,7 +249,7 @@ class SentryWebFluxTracingFilterTest { verify(fixture.chain).filter(fixture.exchange) verify(fixture.scopes).isEnabled - verify(fixture.scopes, times(2)).options + verify(fixture.scopes, times(3)).options verify(fixture.scopes).continueTrace(anyOrNull(), anyOrNull()) verify(fixture.scopes).addBreadcrumb(any(), any()) verify(fixture.scopes).configureScope(any()) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index e0eefc49bf..f3a84217b5 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -311,10 +311,20 @@ public final class io/sentry/CombinedScopeView : io/sentry/IScope { public fun withTransaction (Lio/sentry/Scope$IWithTransaction;)V } +public abstract interface class io/sentry/CompositePerformanceCollector { + public abstract fun close ()V + public abstract fun onSpanFinished (Lio/sentry/ISpan;)V + public abstract fun onSpanStarted (Lio/sentry/ISpan;)V + public abstract fun start (Lio/sentry/ITransaction;)V + public abstract fun start (Ljava/lang/String;)V + public abstract fun stop (Lio/sentry/ITransaction;)Ljava/util/List; + public abstract fun stop (Ljava/lang/String;)Ljava/util/List; +} + public final class io/sentry/CpuCollectionData { - public fun (JD)V + public fun (DLio/sentry/SentryDate;)V public fun getCpuUsagePercentage ()D - public fun getTimestampMillis ()J + public fun getTimestamp ()Lio/sentry/SentryDate; } public final class io/sentry/CustomSamplingContext { @@ -367,6 +377,17 @@ public final class io/sentry/DeduplicateMultithreadedEventProcessor : io/sentry/ public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; } +public final class io/sentry/DefaultCompositePerformanceCollector : io/sentry/CompositePerformanceCollector { + public fun (Lio/sentry/SentryOptions;)V + public fun close ()V + public fun onSpanFinished (Lio/sentry/ISpan;)V + public fun onSpanStarted (Lio/sentry/ISpan;)V + public fun start (Lio/sentry/ITransaction;)V + public fun start (Ljava/lang/String;)V + public fun stop (Lio/sentry/ITransaction;)Ljava/util/List; + public fun stop (Ljava/lang/String;)Ljava/util/List; +} + public final class io/sentry/DefaultScopesStorage : io/sentry/IScopesStorage { public fun ()V public fun close ()V @@ -377,16 +398,7 @@ public final class io/sentry/DefaultScopesStorage : io/sentry/IScopesStorage { public final class io/sentry/DefaultSpanFactory : io/sentry/ISpanFactory { public fun ()V public fun createSpan (Lio/sentry/IScopes;Lio/sentry/SpanOptions;Lio/sentry/SpanContext;Lio/sentry/ISpan;)Lio/sentry/ISpan; - public fun createTransaction (Lio/sentry/TransactionContext;Lio/sentry/IScopes;Lio/sentry/TransactionOptions;Lio/sentry/TransactionPerformanceCollector;)Lio/sentry/ITransaction; -} - -public final class io/sentry/DefaultTransactionPerformanceCollector : io/sentry/TransactionPerformanceCollector { - public fun (Lio/sentry/SentryOptions;)V - public fun close ()V - public fun onSpanFinished (Lio/sentry/ISpan;)V - public fun onSpanStarted (Lio/sentry/ISpan;)V - public fun start (Lio/sentry/ITransaction;)V - public fun stop (Lio/sentry/ITransaction;)Ljava/util/List; + public fun createTransaction (Lio/sentry/TransactionContext;Lio/sentry/IScopes;Lio/sentry/TransactionOptions;Lio/sentry/CompositePerformanceCollector;)Lio/sentry/ITransaction; } public final class io/sentry/DiagnosticLogger : io/sentry/ILogger { @@ -560,6 +572,7 @@ public final class io/sentry/HubAdapter : io/sentry/IHub { public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureProfileChunk (Lio/sentry/ProfileChunk;)Lio/sentry/protocol/SentryId; public fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V @@ -606,8 +619,10 @@ public final class io/sentry/HubAdapter : io/sentry/IHub { public fun setTag (Ljava/lang/String;Ljava/lang/String;)V public fun setTransaction (Ljava/lang/String;)V public fun setUser (Lio/sentry/protocol/User;)V + public fun startProfiler ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfiler ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -625,6 +640,7 @@ public final class io/sentry/HubScopesWrapper : io/sentry/IHub { public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureProfileChunk (Lio/sentry/ProfileChunk;)Lio/sentry/protocol/SentryId; public fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V @@ -670,8 +686,10 @@ public final class io/sentry/HubScopesWrapper : io/sentry/IHub { public fun setTag (Ljava/lang/String;Ljava/lang/String;)V public fun setTransaction (Ljava/lang/String;)V public fun setUser (Lio/sentry/protocol/User;)V + public fun startProfiler ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfiler ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -698,8 +716,8 @@ public abstract interface class io/sentry/IConnectionStatusProvider$IConnectionS public abstract interface class io/sentry/IContinuousProfiler { public abstract fun close ()V + public abstract fun getProfilerId ()Lio/sentry/protocol/SentryId; public abstract fun isRunning ()Z - public abstract fun setScopes (Lio/sentry/IScopes;)V public abstract fun start ()V public abstract fun stop ()V } @@ -856,6 +874,7 @@ public abstract interface class io/sentry/IScopes { public fun captureMessage (Ljava/lang/String;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public abstract fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public abstract fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public abstract fun captureProfileChunk (Lio/sentry/ProfileChunk;)Lio/sentry/protocol/SentryId; public abstract fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;)Lio/sentry/protocol/SentryId; @@ -905,11 +924,13 @@ public abstract interface class io/sentry/IScopes { public abstract fun setTag (Ljava/lang/String;Ljava/lang/String;)V public abstract fun setTransaction (Ljava/lang/String;)V public abstract fun setUser (Lio/sentry/protocol/User;)V + public abstract fun startProfiler ()V public abstract fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;)Lio/sentry/ITransaction; public abstract fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; public fun startTransaction (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ITransaction; public fun startTransaction (Ljava/lang/String;Ljava/lang/String;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public abstract fun stopProfiler ()V public abstract fun withIsolationScope (Lio/sentry/ScopeCallback;)V public abstract fun withScope (Lio/sentry/ScopeCallback;)V } @@ -934,6 +955,7 @@ public abstract interface class io/sentry/ISentryClient { public fun captureException (Ljava/lang/Throwable;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; 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 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 public abstract fun captureSession (Lio/sentry/Session;Lio/sentry/Hint;)V @@ -1014,7 +1036,7 @@ public abstract interface class io/sentry/ISpan { public abstract interface class io/sentry/ISpanFactory { public abstract fun createSpan (Lio/sentry/IScopes;Lio/sentry/SpanOptions;Lio/sentry/SpanContext;Lio/sentry/ISpan;)Lio/sentry/ISpan; - public abstract fun createTransaction (Lio/sentry/TransactionContext;Lio/sentry/IScopes;Lio/sentry/TransactionOptions;Lio/sentry/TransactionPerformanceCollector;)Lio/sentry/ITransaction; + public abstract fun createTransaction (Lio/sentry/TransactionContext;Lio/sentry/IScopes;Lio/sentry/TransactionOptions;Lio/sentry/CompositePerformanceCollector;)Lio/sentry/ITransaction; } public abstract interface class io/sentry/ITransaction : io/sentry/ISpan { @@ -1248,9 +1270,9 @@ public final class io/sentry/MeasurementUnit$Information : java/lang/Enum, io/se } public final class io/sentry/MemoryCollectionData { - public fun (JJ)V - public fun (JJJ)V - public fun getTimestampMillis ()J + public fun (JJLio/sentry/SentryDate;)V + public fun (JLio/sentry/SentryDate;)V + public fun getTimestamp ()Lio/sentry/SentryDate; public fun getUsedHeapMemory ()J public fun getUsedNativeMemory ()J } @@ -1354,6 +1376,17 @@ public final class io/sentry/MonitorScheduleUnit : java/lang/Enum { public static fun values ()[Lio/sentry/MonitorScheduleUnit; } +public final class io/sentry/NoOpCompositePerformanceCollector : io/sentry/CompositePerformanceCollector { + public fun close ()V + public static fun getInstance ()Lio/sentry/NoOpCompositePerformanceCollector; + public fun onSpanFinished (Lio/sentry/ISpan;)V + public fun onSpanStarted (Lio/sentry/ISpan;)V + public fun start (Lio/sentry/ITransaction;)V + public fun start (Ljava/lang/String;)V + public fun stop (Lio/sentry/ITransaction;)Ljava/util/List; + public fun stop (Ljava/lang/String;)Ljava/util/List; +} + public final class io/sentry/NoOpConnectionStatusProvider : io/sentry/IConnectionStatusProvider { public fun ()V public fun addConnectionStatusObserver (Lio/sentry/IConnectionStatusProvider$IConnectionStatusObserver;)Z @@ -1365,8 +1398,8 @@ public final class io/sentry/NoOpConnectionStatusProvider : io/sentry/IConnectio public final class io/sentry/NoOpContinuousProfiler : io/sentry/IContinuousProfiler { public fun close ()V public static fun getInstance ()Lio/sentry/NoOpContinuousProfiler; + public fun getProfilerId ()Lio/sentry/protocol/SentryId; public fun isRunning ()Z - public fun setScopes (Lio/sentry/IScopes;)V public fun start ()V public fun stop ()V } @@ -1388,6 +1421,7 @@ public final class io/sentry/NoOpHub : io/sentry/IHub { public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureProfileChunk (Lio/sentry/ProfileChunk;)Lio/sentry/protocol/SentryId; public fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V @@ -1435,8 +1469,10 @@ public final class io/sentry/NoOpHub : io/sentry/IHub { public fun setTag (Ljava/lang/String;Ljava/lang/String;)V public fun setTransaction (Ljava/lang/String;)V public fun setUser (Lio/sentry/protocol/User;)V + public fun startProfiler ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfiler ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -1547,6 +1583,7 @@ public final class io/sentry/NoOpScopes : io/sentry/IScopes { public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureProfileChunk (Lio/sentry/ProfileChunk;)Lio/sentry/protocol/SentryId; public fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V @@ -1594,8 +1631,10 @@ public final class io/sentry/NoOpScopes : io/sentry/IScopes { public fun setTag (Ljava/lang/String;Ljava/lang/String;)V public fun setTransaction (Ljava/lang/String;)V public fun setUser (Lio/sentry/protocol/User;)V + public fun startProfiler ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfiler ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -1655,7 +1694,7 @@ public final class io/sentry/NoOpSpan : io/sentry/ISpan { public final class io/sentry/NoOpSpanFactory : io/sentry/ISpanFactory { public fun createSpan (Lio/sentry/IScopes;Lio/sentry/SpanOptions;Lio/sentry/SpanContext;Lio/sentry/ISpan;)Lio/sentry/ISpan; - public fun createTransaction (Lio/sentry/TransactionContext;Lio/sentry/IScopes;Lio/sentry/TransactionOptions;Lio/sentry/TransactionPerformanceCollector;)Lio/sentry/ITransaction; + public fun createTransaction (Lio/sentry/TransactionContext;Lio/sentry/IScopes;Lio/sentry/TransactionOptions;Lio/sentry/CompositePerformanceCollector;)Lio/sentry/ITransaction; public static fun getInstance ()Lio/sentry/NoOpSpanFactory; } @@ -1712,15 +1751,6 @@ public final class io/sentry/NoOpTransaction : io/sentry/ITransaction { public fun updateEndDate (Lio/sentry/SentryDate;)Z } -public final class io/sentry/NoOpTransactionPerformanceCollector : io/sentry/TransactionPerformanceCollector { - public fun close ()V - public static fun getInstance ()Lio/sentry/NoOpTransactionPerformanceCollector; - public fun onSpanFinished (Lio/sentry/ISpan;)V - public fun onSpanStarted (Lio/sentry/ISpan;)V - public fun start (Lio/sentry/ITransaction;)V - public fun stop (Lio/sentry/ITransaction;)Ljava/util/List; -} - public final class io/sentry/NoOpTransactionProfiler : io/sentry/ITransactionProfiler { public fun bindTransaction (Lio/sentry/ITransaction;)V public fun close ()V @@ -1806,6 +1836,78 @@ public final class io/sentry/PerformanceCollectionData { public fun getMemoryData ()Lio/sentry/MemoryCollectionData; } +public final class io/sentry/ProfileChunk : io/sentry/JsonSerializable, io/sentry/JsonUnknown { + public fun ()V + public fun (Lio/sentry/protocol/SentryId;Lio/sentry/protocol/SentryId;Ljava/io/File;Ljava/util/Map;Lio/sentry/SentryOptions;)V + public fun equals (Ljava/lang/Object;)Z + public fun getChunkId ()Lio/sentry/protocol/SentryId; + public fun getClientSdk ()Lio/sentry/protocol/SdkVersion; + public fun getDebugMeta ()Lio/sentry/protocol/DebugMeta; + public fun getEnvironment ()Ljava/lang/String; + public fun getMeasurements ()Ljava/util/Map; + public fun getPlatform ()Ljava/lang/String; + public fun getProfilerId ()Lio/sentry/protocol/SentryId; + public fun getRelease ()Ljava/lang/String; + public fun getSampledProfile ()Ljava/lang/String; + public fun getTraceFile ()Ljava/io/File; + public fun getUnknown ()Ljava/util/Map; + public fun getVersion ()Ljava/lang/String; + public fun hashCode ()I + public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public fun setDebugMeta (Lio/sentry/protocol/DebugMeta;)V + public fun setSampledProfile (Ljava/lang/String;)V + public fun setUnknown (Ljava/util/Map;)V +} + +public final class io/sentry/ProfileChunk$Builder { + public fun (Lio/sentry/protocol/SentryId;Lio/sentry/protocol/SentryId;Ljava/util/Map;Ljava/io/File;)V + public fun build (Lio/sentry/SentryOptions;)Lio/sentry/ProfileChunk; +} + +public final class io/sentry/ProfileChunk$Deserializer : io/sentry/JsonDeserializer { + public fun ()V + public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/ProfileChunk; + public synthetic fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; +} + +public final class io/sentry/ProfileChunk$JsonKeys { + public static final field CHUNK_ID Ljava/lang/String; + public static final field CLIENT_SDK Ljava/lang/String; + public static final field DEBUG_META Ljava/lang/String; + public static final field ENVIRONMENT Ljava/lang/String; + public static final field MEASUREMENTS Ljava/lang/String; + public static final field PLATFORM Ljava/lang/String; + public static final field PROFILER_ID Ljava/lang/String; + public static final field RELEASE Ljava/lang/String; + public static final field SAMPLED_PROFILE Ljava/lang/String; + public static final field VERSION Ljava/lang/String; + public fun ()V +} + +public final class io/sentry/ProfileContext : io/sentry/JsonSerializable, io/sentry/JsonUnknown { + public static final field TYPE Ljava/lang/String; + public fun ()V + public fun (Lio/sentry/ProfileContext;)V + public fun (Lio/sentry/protocol/SentryId;)V + public fun equals (Ljava/lang/Object;)Z + public fun getProfilerId ()Lio/sentry/protocol/SentryId; + public fun getUnknown ()Ljava/util/Map; + public fun hashCode ()I + public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public fun setUnknown (Ljava/util/Map;)V +} + +public final class io/sentry/ProfileContext$Deserializer : io/sentry/JsonDeserializer { + public fun ()V + public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/ProfileContext; + public synthetic fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; +} + +public final class io/sentry/ProfileContext$JsonKeys { + public static final field PROFILER_ID Ljava/lang/String; + public fun ()V +} + public final class io/sentry/ProfilingTraceData : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public static final field TRUNCATION_REASON_BACKGROUNDED Ljava/lang/String; public static final field TRUNCATION_REASON_NORMAL Ljava/lang/String; @@ -2137,6 +2239,7 @@ public final class io/sentry/Scopes : io/sentry/IScopes { public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureProfileChunk (Lio/sentry/ProfileChunk;)Lio/sentry/protocol/SentryId; public fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V @@ -2183,8 +2286,10 @@ public final class io/sentry/Scopes : io/sentry/IScopes { public fun setTag (Ljava/lang/String;Ljava/lang/String;)V public fun setTransaction (Ljava/lang/String;)V public fun setUser (Lio/sentry/protocol/User;)V + public fun startProfiler ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfiler ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -2201,6 +2306,7 @@ public final class io/sentry/ScopesAdapter : io/sentry/IScopes { public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureProfileChunk (Lio/sentry/ProfileChunk;)Lio/sentry/protocol/SentryId; public fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V @@ -2247,8 +2353,10 @@ public final class io/sentry/ScopesAdapter : io/sentry/IScopes { public fun setTag (Ljava/lang/String;Ljava/lang/String;)V public fun setTransaction (Ljava/lang/String;)V public fun setUser (Lio/sentry/protocol/User;)V + public fun startProfiler ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfiler ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -2351,12 +2459,14 @@ public final class io/sentry/Sentry { public static fun setTag (Ljava/lang/String;Ljava/lang/String;)V public static fun setTransaction (Ljava/lang/String;)V public static fun setUser (Lio/sentry/protocol/User;)V + public static fun startProfiler ()V public static fun startSession ()V public static fun startTransaction (Lio/sentry/TransactionContext;)Lio/sentry/ITransaction; public static fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; public static fun startTransaction (Ljava/lang/String;Ljava/lang/String;)Lio/sentry/ITransaction; public static fun startTransaction (Ljava/lang/String;Ljava/lang/String;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; public static fun startTransaction (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public static fun stopProfiler ()V public static fun withIsolationScope (Lio/sentry/ScopeCallback;)V public static fun withScope (Lio/sentry/ScopeCallback;)V } @@ -2372,10 +2482,12 @@ public final class io/sentry/SentryAppStartProfilingOptions : io/sentry/JsonSeri public fun getProfilingTracesHz ()I public fun getTraceSampleRate ()Ljava/lang/Double; public fun getUnknown ()Ljava/util/Map; + public fun isContinuousProfilingEnabled ()Z public fun isProfileSampled ()Z public fun isProfilingEnabled ()Z public fun isTraceSampled ()Z public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public fun setContinuousProfilingEnabled (Z)V public fun setProfileSampleRate (Ljava/lang/Double;)V public fun setProfileSampled (Z)V public fun setProfilingEnabled (Z)V @@ -2393,6 +2505,7 @@ public final class io/sentry/SentryAppStartProfilingOptions$Deserializer : io/se } public final class io/sentry/SentryAppStartProfilingOptions$JsonKeys { + public static final field IS_CONTINUOUS_PROFILING_ENABLED Ljava/lang/String; public static final field IS_PROFILING_ENABLED Ljava/lang/String; public static final field PROFILE_SAMPLED Ljava/lang/String; public static final field PROFILE_SAMPLE_RATE Ljava/lang/String; @@ -2485,6 +2598,7 @@ public final class io/sentry/SentryClient : io/sentry/ISentryClient { 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; + 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 public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/IScope;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; @@ -2564,6 +2678,7 @@ public final class io/sentry/SentryEnvelopeItem { public static fun fromCheckIn (Lio/sentry/ISerializer;Lio/sentry/CheckIn;)Lio/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 fromProfileChunk (Lio/sentry/ProfileChunk;Lio/sentry/ISerializer;)Lio/sentry/SentryEnvelopeItem; public static fun fromProfilingTrace (Lio/sentry/ProfilingTraceData;JLio/sentry/ISerializer;)Lio/sentry/SentryEnvelopeItem; public static fun fromReplay (Lio/sentry/ISerializer;Lio/sentry/ILogger;Lio/sentry/SentryReplayEvent;Lio/sentry/ReplayRecording;Z)Lio/sentry/SentryEnvelopeItem; public static fun fromSession (Lio/sentry/ISerializer;Lio/sentry/Session;)Lio/sentry/SentryEnvelopeItem; @@ -2695,6 +2810,7 @@ public final class io/sentry/SentryItemType : java/lang/Enum, io/sentry/JsonSeri public static final field Event Lio/sentry/SentryItemType; public static final field Feedback Lio/sentry/SentryItemType; public static final field Profile Lio/sentry/SentryItemType; + public static final field ProfileChunk Lio/sentry/SentryItemType; public static final field ReplayEvent Lio/sentry/SentryItemType; public static final field ReplayRecording Lio/sentry/SentryItemType; public static final field ReplayVideo Lio/sentry/SentryItemType; @@ -2816,9 +2932,11 @@ public class io/sentry/SentryOptions { public fun getBundleIds ()Ljava/util/Set; public fun getCacheDirPath ()Ljava/lang/String; public fun getClientReportRecorder ()Lio/sentry/clientreport/IClientReportRecorder; + public fun getCompositePerformanceCollector ()Lio/sentry/CompositePerformanceCollector; public fun getConnectionStatusProvider ()Lio/sentry/IConnectionStatusProvider; public fun getConnectionTimeoutMillis ()I public fun getContextTags ()Ljava/util/List; + public fun getContinuousProfiler ()Lio/sentry/IContinuousProfiler; public fun getCron ()Lio/sentry/SentryOptions$Cron; public fun getDateProvider ()Lio/sentry/SentryDateProvider; public fun getDebugMetaLoader ()Lio/sentry/internal/debugmeta/IDebugMetaLoader; @@ -2885,7 +3003,6 @@ public class io/sentry/SentryOptions { public fun getTracePropagationTargets ()Ljava/util/List; public fun getTracesSampleRate ()Ljava/lang/Double; public fun getTracesSampler ()Lio/sentry/SentryOptions$TracesSamplerCallback; - public fun getTransactionPerformanceCollector ()Lio/sentry/TransactionPerformanceCollector; public fun getTransactionProfiler ()Lio/sentry/ITransactionProfiler; public fun getTransportFactory ()Lio/sentry/ITransportFactory; public fun getTransportGate ()Lio/sentry/transport/ITransportGate; @@ -2893,6 +3010,7 @@ public class io/sentry/SentryOptions { public fun isAttachServerName ()Z public fun isAttachStacktrace ()Z public fun isAttachThreads ()Z + public fun isContinuousProfilingEnabled ()Z public fun isDebug ()Z public fun isEnableAppStartProfiling ()Z public fun isEnableAutoSessionTracking ()Z @@ -2929,8 +3047,10 @@ public class io/sentry/SentryOptions { public fun setBeforeSend (Lio/sentry/SentryOptions$BeforeSendCallback;)V public fun setBeforeSendTransaction (Lio/sentry/SentryOptions$BeforeSendTransactionCallback;)V public fun setCacheDirPath (Ljava/lang/String;)V + public fun setCompositePerformanceCollector (Lio/sentry/CompositePerformanceCollector;)V public fun setConnectionStatusProvider (Lio/sentry/IConnectionStatusProvider;)V public fun setConnectionTimeoutMillis (I)V + public fun setContinuousProfiler (Lio/sentry/IContinuousProfiler;)V public fun setCron (Lio/sentry/SentryOptions$Cron;)V public fun setDateProvider (Lio/sentry/SentryDateProvider;)V public fun setDebug (Z)V @@ -3009,7 +3129,6 @@ public class io/sentry/SentryOptions { public fun setTraceSampling (Z)V public fun setTracesSampleRate (Ljava/lang/Double;)V public fun setTracesSampler (Lio/sentry/SentryOptions$TracesSamplerCallback;)V - public fun setTransactionPerformanceCollector (Lio/sentry/TransactionPerformanceCollector;)V public fun setTransactionProfiler (Lio/sentry/ITransactionProfiler;)V public fun setTransportFactory (Lio/sentry/ITransportFactory;)V public fun setTransportGate (Lio/sentry/transport/ITransportGate;)V @@ -3496,6 +3615,7 @@ public abstract interface class io/sentry/SpanDataConvention { public static final field HTTP_RESPONSE_CONTENT_LENGTH_KEY Ljava/lang/String; public static final field HTTP_START_TIMESTAMP Ljava/lang/String; public static final field HTTP_STATUS_CODE_KEY Ljava/lang/String; + public static final field PROFILER_ID Ljava/lang/String; public static final field THREAD_ID Ljava/lang/String; public static final field THREAD_NAME Ljava/lang/String; } @@ -3679,14 +3799,6 @@ public final class io/sentry/TransactionOptions : io/sentry/SpanOptions { public fun setWaitForChildren (Z)V } -public abstract interface class io/sentry/TransactionPerformanceCollector { - public abstract fun close ()V - public abstract fun onSpanFinished (Lio/sentry/ISpan;)V - public abstract fun onSpanStarted (Lio/sentry/ISpan;)V - public abstract fun start (Lio/sentry/ITransaction;)V - public abstract fun stop (Lio/sentry/ITransaction;)Ljava/util/List; -} - public final class io/sentry/TypeCheckHint { public static final field ANDROID_ACTIVITY Ljava/lang/String; public static final field ANDROID_CONFIGURATION Ljava/lang/String; @@ -4244,9 +4356,10 @@ public final class io/sentry/profilemeasurements/ProfileMeasurement$JsonKeys { public final class io/sentry/profilemeasurements/ProfileMeasurementValue : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun ()V - public fun (Ljava/lang/Long;Ljava/lang/Number;)V + public fun (Ljava/lang/Long;Ljava/lang/Number;Lio/sentry/SentryDate;)V public fun equals (Ljava/lang/Object;)Z public fun getRelativeStartNs ()Ljava/lang/String; + public fun getTimestamp ()Ljava/lang/Double; public fun getUnknown ()Ljava/util/Map; public fun getValue ()D public fun hashCode ()I @@ -4262,6 +4375,7 @@ public final class io/sentry/profilemeasurements/ProfileMeasurementValue$Deseria public final class io/sentry/profilemeasurements/ProfileMeasurementValue$JsonKeys { public static final field START_NS Ljava/lang/String; + public static final field TIMESTAMP Ljava/lang/String; public static final field VALUE Ljava/lang/String; public fun ()V } @@ -4359,6 +4473,7 @@ public class io/sentry/protocol/Contexts : io/sentry/JsonSerializable { public fun getDevice ()Lio/sentry/protocol/Device; public fun getGpu ()Lio/sentry/protocol/Gpu; public fun getOperatingSystem ()Lio/sentry/protocol/OperatingSystem; + public fun getProfile ()Lio/sentry/ProfileContext; public fun getResponse ()Lio/sentry/protocol/Response; public fun getRuntime ()Lio/sentry/protocol/SentryRuntime; public fun getSize ()I @@ -4377,6 +4492,7 @@ public class io/sentry/protocol/Contexts : io/sentry/JsonSerializable { public fun setDevice (Lio/sentry/protocol/Device;)V public fun setGpu (Lio/sentry/protocol/Gpu;)V public fun setOperatingSystem (Lio/sentry/protocol/OperatingSystem;)V + public fun setProfile (Lio/sentry/ProfileContext;)V public fun setResponse (Lio/sentry/protocol/Response;)V public fun setRuntime (Lio/sentry/protocol/SentryRuntime;)V public fun setTrace (Lio/sentry/SpanContext;)V @@ -4439,6 +4555,7 @@ public final class io/sentry/protocol/DebugImage$JsonKeys { public final class io/sentry/protocol/DebugMeta : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun ()V + public static fun buildDebugMeta (Lio/sentry/protocol/DebugMeta;Lio/sentry/SentryOptions;)Lio/sentry/protocol/DebugMeta; public fun getImages ()Ljava/util/List; public fun getSdkInfo ()Lio/sentry/protocol/SdkInfo; public fun getUnknown ()Ljava/util/Map; @@ -6253,6 +6370,7 @@ public final class io/sentry/util/UrlUtils$UrlDetails { public abstract interface class io/sentry/util/thread/IThreadChecker { public abstract fun currentThreadSystemId ()J + public abstract fun getCurrentThreadName ()Ljava/lang/String; public abstract fun isMainThread ()Z public abstract fun isMainThread (J)Z public abstract fun isMainThread (Lio/sentry/protocol/SentryThread;)Z @@ -6262,6 +6380,7 @@ public abstract interface class io/sentry/util/thread/IThreadChecker { public final class io/sentry/util/thread/NoOpThreadChecker : io/sentry/util/thread/IThreadChecker { public fun ()V public fun currentThreadSystemId ()J + public fun getCurrentThreadName ()Ljava/lang/String; public static fun getInstance ()Lio/sentry/util/thread/NoOpThreadChecker; public fun isMainThread ()Z public fun isMainThread (J)Z @@ -6271,6 +6390,7 @@ public final class io/sentry/util/thread/NoOpThreadChecker : io/sentry/util/thre public final class io/sentry/util/thread/ThreadChecker : io/sentry/util/thread/IThreadChecker { public fun currentThreadSystemId ()J + public fun getCurrentThreadName ()Ljava/lang/String; public static fun getInstance ()Lio/sentry/util/thread/ThreadChecker; public fun isMainThread ()Z public fun isMainThread (J)Z diff --git a/sentry/src/main/java/io/sentry/TransactionPerformanceCollector.java b/sentry/src/main/java/io/sentry/CompositePerformanceCollector.java similarity index 61% rename from sentry/src/main/java/io/sentry/TransactionPerformanceCollector.java rename to sentry/src/main/java/io/sentry/CompositePerformanceCollector.java index 7880d61197..e6238679a7 100644 --- a/sentry/src/main/java/io/sentry/TransactionPerformanceCollector.java +++ b/sentry/src/main/java/io/sentry/CompositePerformanceCollector.java @@ -5,10 +5,14 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public interface TransactionPerformanceCollector { +public interface CompositePerformanceCollector { + /** Starts collecting performance data and span related data (e.g. slow/frozen frames). */ void start(@NotNull ITransaction transaction); + /** Starts collecting performance data without span related data (e.g. slow/frozen frames). */ + void start(@NotNull String id); + /** * Called whenever a new span (including the top level transaction) is started. * @@ -23,9 +27,14 @@ public interface TransactionPerformanceCollector { */ void onSpanFinished(@NotNull ISpan span); + /** Stops collecting performance data and span related data (e.g. slow/frozen frames). */ @Nullable List stop(@NotNull ITransaction transaction); + /** Stops collecting performance data. */ + @Nullable + List stop(@NotNull String id); + /** Cancel the collector and stops it. Used on SDK close. */ @ApiStatus.Internal void close(); diff --git a/sentry/src/main/java/io/sentry/CpuCollectionData.java b/sentry/src/main/java/io/sentry/CpuCollectionData.java index 081063a53f..bcbab7c136 100644 --- a/sentry/src/main/java/io/sentry/CpuCollectionData.java +++ b/sentry/src/main/java/io/sentry/CpuCollectionData.java @@ -1,19 +1,20 @@ package io.sentry; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; @ApiStatus.Internal public final class CpuCollectionData { - final long timestampMillis; final double cpuUsagePercentage; + final @NotNull SentryDate timestamp; - public CpuCollectionData(final long timestampMillis, final double cpuUsagePercentage) { - this.timestampMillis = timestampMillis; + public CpuCollectionData(final double cpuUsagePercentage, final @NotNull SentryDate timestamp) { this.cpuUsagePercentage = cpuUsagePercentage; + this.timestamp = timestamp; } - public long getTimestampMillis() { - return timestampMillis; + public @NotNull SentryDate getTimestamp() { + return timestamp; } public double getCpuUsagePercentage() { diff --git a/sentry/src/main/java/io/sentry/DefaultTransactionPerformanceCollector.java b/sentry/src/main/java/io/sentry/DefaultCompositePerformanceCollector.java similarity index 87% rename from sentry/src/main/java/io/sentry/DefaultTransactionPerformanceCollector.java rename to sentry/src/main/java/io/sentry/DefaultCompositePerformanceCollector.java index 9489842b5c..ae99fe00c7 100644 --- a/sentry/src/main/java/io/sentry/DefaultTransactionPerformanceCollector.java +++ b/sentry/src/main/java/io/sentry/DefaultCompositePerformanceCollector.java @@ -15,8 +15,7 @@ import org.jetbrains.annotations.Nullable; @ApiStatus.Internal -public final class DefaultTransactionPerformanceCollector - implements TransactionPerformanceCollector { +public final class DefaultCompositePerformanceCollector implements CompositePerformanceCollector { private static final long TRANSACTION_COLLECTION_INTERVAL_MILLIS = 100; private static final long TRANSACTION_COLLECTION_TIMEOUT_MILLIS = 30000; private final @NotNull AutoClosableReentrantLock timerLock = new AutoClosableReentrantLock(); @@ -31,7 +30,7 @@ public final class DefaultTransactionPerformanceCollector private final @NotNull AtomicBoolean isStarted = new AtomicBoolean(false); private long lastCollectionTimestamp = 0; - public DefaultTransactionPerformanceCollector(final @NotNull SentryOptions options) { + public DefaultCompositePerformanceCollector(final @NotNull SentryOptions options) { this.options = Objects.requireNonNull(options, "The options object is required."); this.snapshotCollectors = new ArrayList<>(); this.continuousCollectors = new ArrayList<>(); @@ -82,6 +81,23 @@ public void start(final @NotNull ITransaction transaction) { e); } } + start(transaction.getEventId().toString()); + } + + @Override + public void start(final @NotNull String id) { + if (hasNoCollectors) { + options + .getLogger() + .log( + SentryLevel.INFO, + "No collector found. Performance stats will not be captured during transactions."); + return; + } + + if (!performanceDataMap.containsKey(id)) { + performanceDataMap.put(id, new ArrayList<>()); + } if (!isStarted.getAndSet(true)) { try (final @NotNull ISentryLifecycleToken ignored = timerLock.acquire()) { if (timer == null) { @@ -110,7 +126,7 @@ public void run() { // The timer is scheduled to run every 100ms on average. In case it takes longer, // subsequent tasks are executed more quickly. If two tasks are scheduled to run in // less than 10ms, the measurement that we collect is not meaningful, so we skip it - if (now - lastCollectionTimestamp < 10) { + if (now - lastCollectionTimestamp <= 10) { return; } lastCollectionTimestamp = now; @@ -157,14 +173,18 @@ public void onSpanFinished(@NotNull ISpan span) { transaction.getName(), transaction.getSpanContext().getTraceId().toString()); - final @Nullable List data = - performanceDataMap.remove(transaction.getEventId().toString()); - for (final @NotNull IPerformanceContinuousCollector collector : continuousCollectors) { collector.onSpanFinished(transaction); } - // close if they are no more remaining transactions + return stop(transaction.getEventId().toString()); + } + + @Override + public @Nullable List stop(final @NotNull String id) { + final @Nullable List data = performanceDataMap.remove(id); + + // close if they are no more running requests if (performanceDataMap.isEmpty()) { close(); } diff --git a/sentry/src/main/java/io/sentry/DefaultSpanFactory.java b/sentry/src/main/java/io/sentry/DefaultSpanFactory.java index 7ac2448849..6054ed5166 100644 --- a/sentry/src/main/java/io/sentry/DefaultSpanFactory.java +++ b/sentry/src/main/java/io/sentry/DefaultSpanFactory.java @@ -11,8 +11,8 @@ public final class DefaultSpanFactory implements ISpanFactory { final @NotNull TransactionContext context, final @NotNull IScopes scopes, final @NotNull TransactionOptions transactionOptions, - final @Nullable TransactionPerformanceCollector transactionPerformanceCollector) { - return new SentryTracer(context, scopes, transactionOptions, transactionPerformanceCollector); + final @Nullable CompositePerformanceCollector compositePerformanceCollector) { + return new SentryTracer(context, scopes, transactionOptions, compositePerformanceCollector); } @Override diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index fc2f9c15dc..537bcdf104 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -277,6 +277,22 @@ public boolean isAncestorOf(final @Nullable IScopes otherScopes) { return Sentry.startTransaction(transactionContext, transactionOptions); } + @Override + public void startProfiler() { + Sentry.startProfiler(); + } + + @Override + public void stopProfiler() { + Sentry.stopProfiler(); + } + + @Override + public @NotNull SentryId captureProfileChunk( + final @NotNull ProfileChunk profilingContinuousData) { + return Sentry.getCurrentScopes().captureProfileChunk(profilingContinuousData); + } + @Override public void setSpanContext( final @NotNull Throwable throwable, diff --git a/sentry/src/main/java/io/sentry/HubScopesWrapper.java b/sentry/src/main/java/io/sentry/HubScopesWrapper.java index 591852a9ad..d6755c2c41 100644 --- a/sentry/src/main/java/io/sentry/HubScopesWrapper.java +++ b/sentry/src/main/java/io/sentry/HubScopesWrapper.java @@ -265,6 +265,11 @@ public boolean isAncestorOf(final @Nullable IScopes otherScopes) { return scopes.captureTransaction(transaction, traceContext, hint, profilingTraceData); } + @Override + public @NotNull SentryId captureProfileChunk(@NotNull ProfileChunk profileChunk) { + return scopes.captureProfileChunk(profileChunk); + } + @Override public @NotNull ITransaction startTransaction( @NotNull TransactionContext transactionContext, @@ -272,6 +277,16 @@ public boolean isAncestorOf(final @Nullable IScopes otherScopes) { return scopes.startTransaction(transactionContext, transactionOptions); } + @Override + public void startProfiler() { + scopes.startProfiler(); + } + + @Override + public void stopProfiler() { + scopes.stopProfiler(); + } + @ApiStatus.Internal @Override public void setSpanContext( diff --git a/sentry/src/main/java/io/sentry/IContinuousProfiler.java b/sentry/src/main/java/io/sentry/IContinuousProfiler.java index c94eb9bba3..14ce41a815 100644 --- a/sentry/src/main/java/io/sentry/IContinuousProfiler.java +++ b/sentry/src/main/java/io/sentry/IContinuousProfiler.java @@ -1,5 +1,6 @@ package io.sentry; +import io.sentry.protocol.SentryId; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -12,8 +13,9 @@ public interface IContinuousProfiler { void stop(); - void setScopes(final @NotNull IScopes scopes); - /** Cancel the profiler and stops it. Used on SDK close. */ void close(); + + @NotNull + SentryId getProfilerId(); } diff --git a/sentry/src/main/java/io/sentry/IScopes.java b/sentry/src/main/java/io/sentry/IScopes.java index e07de9c327..59a577f04b 100644 --- a/sentry/src/main/java/io/sentry/IScopes.java +++ b/sentry/src/main/java/io/sentry/IScopes.java @@ -527,6 +527,16 @@ default SentryId captureTransaction(@NotNull SentryTransaction transaction, @Nul return captureTransaction(transaction, traceContext, null); } + /** + * Captures the profile chunk and enqueues it for sending to Sentry server. + * + * @param profileChunk the continuous profiling payload + * @return the profile chunk id + */ + @ApiStatus.Internal + @NotNull + SentryId captureProfileChunk(final @NotNull ProfileChunk profileChunk); + /** * Creates a Transaction and returns the instance. * @@ -582,6 +592,10 @@ ITransaction startTransaction( final @NotNull TransactionContext transactionContext, final @NotNull TransactionOptions transactionOptions); + void startProfiler(); + + void stopProfiler(); + /** * Associates {@link ISpan} and the transaction name with the {@link Throwable}. Used to determine * in which trace the exception has been thrown in framework integrations. diff --git a/sentry/src/main/java/io/sentry/ISentryClient.java b/sentry/src/main/java/io/sentry/ISentryClient.java index 017b283d57..22389f2b6e 100644 --- a/sentry/src/main/java/io/sentry/ISentryClient.java +++ b/sentry/src/main/java/io/sentry/ISentryClient.java @@ -277,6 +277,17 @@ SentryId captureTransaction( return captureTransaction(transaction, null, null, null); } + /** + * Captures the profile chunk and enqueues it for sending to Sentry server. + * + * @param profilingContinuousData the continuous profiling payload + * @return the profile chunk id + */ + @ApiStatus.Internal + @NotNull + SentryId captureProfileChunk( + final @NotNull ProfileChunk profilingContinuousData, final @Nullable IScope scope); + @NotNull @ApiStatus.Experimental SentryId captureCheckIn(@NotNull CheckIn checkIn, @Nullable IScope scope, @Nullable Hint hint); diff --git a/sentry/src/main/java/io/sentry/ISpanFactory.java b/sentry/src/main/java/io/sentry/ISpanFactory.java index 1e429e2fea..9b7c6afaba 100644 --- a/sentry/src/main/java/io/sentry/ISpanFactory.java +++ b/sentry/src/main/java/io/sentry/ISpanFactory.java @@ -11,7 +11,7 @@ ITransaction createTransaction( @NotNull TransactionContext context, @NotNull IScopes scopes, @NotNull TransactionOptions transactionOptions, - @Nullable TransactionPerformanceCollector transactionPerformanceCollector); + @Nullable CompositePerformanceCollector compositePerformanceCollector); @NotNull ISpan createSpan( diff --git a/sentry/src/main/java/io/sentry/JavaMemoryCollector.java b/sentry/src/main/java/io/sentry/JavaMemoryCollector.java index cdde808ba5..9ede59ba07 100644 --- a/sentry/src/main/java/io/sentry/JavaMemoryCollector.java +++ b/sentry/src/main/java/io/sentry/JavaMemoryCollector.java @@ -13,9 +13,9 @@ public void setup() {} @Override public void collect(final @NotNull PerformanceCollectionData performanceCollectionData) { - final long now = System.currentTimeMillis(); final long usedMemory = runtime.totalMemory() - runtime.freeMemory(); - MemoryCollectionData memoryData = new MemoryCollectionData(now, usedMemory); + MemoryCollectionData memoryData = + new MemoryCollectionData(usedMemory, new SentryNanotimeDate()); performanceCollectionData.addMemoryData(memoryData); } } diff --git a/sentry/src/main/java/io/sentry/JsonSerializer.java b/sentry/src/main/java/io/sentry/JsonSerializer.java index 09773dc617..bb2d356a1d 100644 --- a/sentry/src/main/java/io/sentry/JsonSerializer.java +++ b/sentry/src/main/java/io/sentry/JsonSerializer.java @@ -89,6 +89,8 @@ public JsonSerializer(@NotNull SentryOptions options) { deserializersByClass.put(Mechanism.class, new Mechanism.Deserializer()); deserializersByClass.put(Message.class, new Message.Deserializer()); deserializersByClass.put(OperatingSystem.class, new OperatingSystem.Deserializer()); + deserializersByClass.put(ProfileChunk.class, new ProfileChunk.Deserializer()); + deserializersByClass.put(ProfileContext.class, new ProfileContext.Deserializer()); deserializersByClass.put(ProfilingTraceData.class, new ProfilingTraceData.Deserializer()); deserializersByClass.put( ProfilingTransactionData.class, new ProfilingTransactionData.Deserializer()); diff --git a/sentry/src/main/java/io/sentry/MainEventProcessor.java b/sentry/src/main/java/io/sentry/MainEventProcessor.java index a5cbacc4df..d76f25c562 100644 --- a/sentry/src/main/java/io/sentry/MainEventProcessor.java +++ b/sentry/src/main/java/io/sentry/MainEventProcessor.java @@ -2,7 +2,6 @@ import io.sentry.hints.AbnormalExit; import io.sentry.hints.Cached; -import io.sentry.protocol.DebugImage; import io.sentry.protocol.DebugMeta; import io.sentry.protocol.SentryException; import io.sentry.protocol.SentryTransaction; @@ -68,34 +67,8 @@ public MainEventProcessor(final @NotNull SentryOptions options) { } private void setDebugMeta(final @NotNull SentryBaseEvent event) { - final @NotNull List debugImages = new ArrayList<>(); - - if (options.getProguardUuid() != null) { - final DebugImage proguardMappingImage = new DebugImage(); - proguardMappingImage.setType(DebugImage.PROGUARD); - proguardMappingImage.setUuid(options.getProguardUuid()); - debugImages.add(proguardMappingImage); - } - - for (final @NotNull String bundleId : options.getBundleIds()) { - final DebugImage sourceBundleImage = new DebugImage(); - sourceBundleImage.setType(DebugImage.JVM); - sourceBundleImage.setDebugId(bundleId); - debugImages.add(sourceBundleImage); - } - - if (!debugImages.isEmpty()) { - DebugMeta debugMeta = event.getDebugMeta(); - - if (debugMeta == null) { - debugMeta = new DebugMeta(); - } - if (debugMeta.getImages() == null) { - debugMeta.setImages(debugImages); - } else { - debugMeta.getImages().addAll(debugImages); - } - + final DebugMeta debugMeta = DebugMeta.buildDebugMeta(event.getDebugMeta(), options); + if (debugMeta != null) { event.setDebugMeta(debugMeta); } } diff --git a/sentry/src/main/java/io/sentry/MemoryCollectionData.java b/sentry/src/main/java/io/sentry/MemoryCollectionData.java index 0fbb66412e..1155e00b4b 100644 --- a/sentry/src/main/java/io/sentry/MemoryCollectionData.java +++ b/sentry/src/main/java/io/sentry/MemoryCollectionData.java @@ -1,26 +1,27 @@ package io.sentry; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; @ApiStatus.Internal public final class MemoryCollectionData { - final long timestampMillis; final long usedHeapMemory; final long usedNativeMemory; + final @NotNull SentryDate timestamp; public MemoryCollectionData( - final long timestampMillis, final long usedHeapMemory, final long usedNativeMemory) { - this.timestampMillis = timestampMillis; + final long usedHeapMemory, final long usedNativeMemory, final @NotNull SentryDate timestamp) { this.usedHeapMemory = usedHeapMemory; this.usedNativeMemory = usedNativeMemory; + this.timestamp = timestamp; } - public MemoryCollectionData(final long timestampMillis, final long usedHeapMemory) { - this(timestampMillis, usedHeapMemory, -1); + public MemoryCollectionData(final long usedHeapMemory, final @NotNull SentryDate timestamp) { + this(usedHeapMemory, -1, timestamp); } - public long getTimestampMillis() { - return timestampMillis; + public @NotNull SentryDate getTimestamp() { + return timestamp; } public long getUsedHeapMemory() { diff --git a/sentry/src/main/java/io/sentry/NoOpTransactionPerformanceCollector.java b/sentry/src/main/java/io/sentry/NoOpCompositePerformanceCollector.java similarity index 51% rename from sentry/src/main/java/io/sentry/NoOpTransactionPerformanceCollector.java rename to sentry/src/main/java/io/sentry/NoOpCompositePerformanceCollector.java index abf5ec5f6a..a159be9182 100644 --- a/sentry/src/main/java/io/sentry/NoOpTransactionPerformanceCollector.java +++ b/sentry/src/main/java/io/sentry/NoOpCompositePerformanceCollector.java @@ -4,20 +4,23 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public final class NoOpTransactionPerformanceCollector implements TransactionPerformanceCollector { +public final class NoOpCompositePerformanceCollector implements CompositePerformanceCollector { - private static final NoOpTransactionPerformanceCollector instance = - new NoOpTransactionPerformanceCollector(); + private static final NoOpCompositePerformanceCollector instance = + new NoOpCompositePerformanceCollector(); - public static NoOpTransactionPerformanceCollector getInstance() { + public static NoOpCompositePerformanceCollector getInstance() { return instance; } - private NoOpTransactionPerformanceCollector() {} + private NoOpCompositePerformanceCollector() {} @Override public void start(@NotNull ITransaction transaction) {} + @Override + public void start(@NotNull String id) {} + @Override public void onSpanStarted(@NotNull ISpan span) {} @@ -29,6 +32,11 @@ public void onSpanFinished(@NotNull ISpan span) {} return null; } + @Override + public @Nullable List stop(@NotNull String id) { + return null; + } + @Override public void close() {} } diff --git a/sentry/src/main/java/io/sentry/NoOpContinuousProfiler.java b/sentry/src/main/java/io/sentry/NoOpContinuousProfiler.java index b17123029f..4ccf7cc681 100644 --- a/sentry/src/main/java/io/sentry/NoOpContinuousProfiler.java +++ b/sentry/src/main/java/io/sentry/NoOpContinuousProfiler.java @@ -1,5 +1,6 @@ package io.sentry; +import io.sentry.protocol.SentryId; import org.jetbrains.annotations.NotNull; public final class NoOpContinuousProfiler implements IContinuousProfiler { @@ -18,9 +19,6 @@ public void start() {} @Override public void stop() {} - @Override - public void setScopes(@NotNull IScopes scopes) {} - @Override public boolean isRunning() { return false; @@ -28,4 +26,9 @@ public boolean isRunning() { @Override public void close() {} + + @Override + public @NotNull SentryId getProfilerId() { + return SentryId.EMPTY_ID; + } } diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index d3e0b010c3..925f1e64ee 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -231,6 +231,11 @@ public boolean isAncestorOf(@Nullable IScopes otherScopes) { return SentryId.EMPTY_ID; } + @Override + public @NotNull SentryId captureProfileChunk(final @NotNull ProfileChunk profileChunk) { + return SentryId.EMPTY_ID; + } + @Override public @NotNull ITransaction startTransaction( @NotNull TransactionContext transactionContext, @@ -238,6 +243,12 @@ public boolean isAncestorOf(@Nullable IScopes otherScopes) { return NoOpTransaction.getInstance(); } + @Override + public void startProfiler() {} + + @Override + public void stopProfiler() {} + @Override public void setSpanContext( final @NotNull Throwable throwable, diff --git a/sentry/src/main/java/io/sentry/NoOpScopes.java b/sentry/src/main/java/io/sentry/NoOpScopes.java index 8255569387..11bae042b0 100644 --- a/sentry/src/main/java/io/sentry/NoOpScopes.java +++ b/sentry/src/main/java/io/sentry/NoOpScopes.java @@ -226,6 +226,11 @@ public boolean isAncestorOf(@Nullable IScopes otherScopes) { return SentryId.EMPTY_ID; } + @Override + public @NotNull SentryId captureProfileChunk(@NotNull ProfileChunk profileChunk) { + return SentryId.EMPTY_ID; + } + @Override public @NotNull ITransaction startTransaction( @NotNull TransactionContext transactionContext, @@ -233,6 +238,12 @@ public boolean isAncestorOf(@Nullable IScopes otherScopes) { return NoOpTransaction.getInstance(); } + @Override + public void startProfiler() {} + + @Override + public void stopProfiler() {} + @Override public void setSpanContext( final @NotNull Throwable throwable, diff --git a/sentry/src/main/java/io/sentry/NoOpSentryClient.java b/sentry/src/main/java/io/sentry/NoOpSentryClient.java index 162b1fae5a..4bee000805 100644 --- a/sentry/src/main/java/io/sentry/NoOpSentryClient.java +++ b/sentry/src/main/java/io/sentry/NoOpSentryClient.java @@ -58,6 +58,12 @@ public SentryId captureEnvelope(@NotNull SentryEnvelope envelope, @Nullable Hint return SentryId.EMPTY_ID; } + @Override + public @NotNull SentryId captureProfileChunk( + final @NotNull ProfileChunk profileChunk, final @Nullable IScope scope) { + return SentryId.EMPTY_ID; + } + @Override @ApiStatus.Experimental public @NotNull SentryId captureCheckIn( diff --git a/sentry/src/main/java/io/sentry/NoOpSpanFactory.java b/sentry/src/main/java/io/sentry/NoOpSpanFactory.java index 05bea4edfe..871e281054 100644 --- a/sentry/src/main/java/io/sentry/NoOpSpanFactory.java +++ b/sentry/src/main/java/io/sentry/NoOpSpanFactory.java @@ -20,7 +20,7 @@ public static NoOpSpanFactory getInstance() { @NotNull TransactionContext context, @NotNull IScopes scopes, @NotNull TransactionOptions transactionOptions, - @Nullable TransactionPerformanceCollector transactionPerformanceCollector) { + @Nullable CompositePerformanceCollector compositePerformanceCollector) { return NoOpTransaction.getInstance(); } diff --git a/sentry/src/main/java/io/sentry/ProfileChunk.java b/sentry/src/main/java/io/sentry/ProfileChunk.java new file mode 100644 index 0000000000..725c151dbd --- /dev/null +++ b/sentry/src/main/java/io/sentry/ProfileChunk.java @@ -0,0 +1,317 @@ +package io.sentry; + +import io.sentry.profilemeasurements.ProfileMeasurement; +import io.sentry.protocol.DebugMeta; +import io.sentry.protocol.SdkVersion; +import io.sentry.protocol.SentryId; +import io.sentry.vendor.gson.stream.JsonToken; +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class ProfileChunk implements JsonUnknown, JsonSerializable { + private @Nullable DebugMeta debugMeta; + private @NotNull SentryId profilerId; + private @NotNull SentryId chunkId; + private @Nullable SdkVersion clientSdk; + private final @NotNull Map measurements; + private @NotNull String platform; + private @NotNull String release; + private @Nullable String environment; + private @NotNull String version; + + private final @NotNull File traceFile; + + /** Profile trace encoded with Base64. */ + private @Nullable String sampledProfile = null; + + private @Nullable Map unknown; + + public ProfileChunk() { + this( + SentryId.EMPTY_ID, + SentryId.EMPTY_ID, + new File("dummy"), + new HashMap<>(), + SentryOptions.empty()); + } + + public ProfileChunk( + final @NotNull SentryId profilerId, + final @NotNull SentryId chunkId, + final @NotNull File traceFile, + final @NotNull Map measurements, + final @NotNull SentryOptions options) { + this.profilerId = profilerId; + this.chunkId = chunkId; + this.traceFile = traceFile; + this.measurements = measurements; + this.debugMeta = null; + this.clientSdk = options.getSdkVersion(); + this.release = options.getRelease() != null ? options.getRelease() : ""; + this.environment = options.getEnvironment(); + this.platform = "android"; + this.version = "2"; + } + + public @NotNull Map getMeasurements() { + return measurements; + } + + public @Nullable DebugMeta getDebugMeta() { + return debugMeta; + } + + public void setDebugMeta(final @Nullable DebugMeta debugMeta) { + this.debugMeta = debugMeta; + } + + public @Nullable SdkVersion getClientSdk() { + return clientSdk; + } + + public @NotNull SentryId getChunkId() { + return chunkId; + } + + public @Nullable String getEnvironment() { + return environment; + } + + public @NotNull String getPlatform() { + return platform; + } + + public @NotNull SentryId getProfilerId() { + return profilerId; + } + + public @NotNull String getRelease() { + return release; + } + + public @Nullable String getSampledProfile() { + return sampledProfile; + } + + public void setSampledProfile(final @Nullable String sampledProfile) { + this.sampledProfile = sampledProfile; + } + + public @NotNull File getTraceFile() { + return traceFile; + } + + public @NotNull String getVersion() { + return version; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ProfileChunk)) return false; + ProfileChunk that = (ProfileChunk) o; + return Objects.equals(debugMeta, that.debugMeta) + && Objects.equals(profilerId, that.profilerId) + && Objects.equals(chunkId, that.chunkId) + && Objects.equals(clientSdk, that.clientSdk) + && Objects.equals(measurements, that.measurements) + && Objects.equals(platform, that.platform) + && Objects.equals(release, that.release) + && Objects.equals(environment, that.environment) + && Objects.equals(version, that.version) + && Objects.equals(sampledProfile, that.sampledProfile) + && Objects.equals(unknown, that.unknown); + } + + @Override + public int hashCode() { + return Objects.hash( + debugMeta, + profilerId, + chunkId, + clientSdk, + measurements, + platform, + release, + environment, + version, + sampledProfile, + unknown); + } + + public static final class Builder { + private final @NotNull SentryId profilerId; + private final @NotNull SentryId chunkId; + private final @NotNull Map measurements; + private final @NotNull File traceFile; + + public Builder( + final @NotNull SentryId profilerId, + final @NotNull SentryId chunkId, + final @NotNull Map measurements, + final @NotNull File traceFile) { + this.profilerId = profilerId; + this.chunkId = chunkId; + this.measurements = new ConcurrentHashMap<>(measurements); + this.traceFile = traceFile; + } + + public ProfileChunk build(SentryOptions options) { + return new ProfileChunk(profilerId, chunkId, traceFile, measurements, options); + } + } + + // JsonSerializable + + public static final class JsonKeys { + public static final String DEBUG_META = "debug_meta"; + public static final String PROFILER_ID = "profiler_id"; + public static final String CHUNK_ID = "chunk_id"; + public static final String CLIENT_SDK = "client_sdk"; + public static final String MEASUREMENTS = "measurements"; + public static final String PLATFORM = "platform"; + public static final String RELEASE = "release"; + public static final String ENVIRONMENT = "environment"; + public static final String VERSION = "version"; + public static final String SAMPLED_PROFILE = "sampled_profile"; + } + + @Override + public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger) + throws IOException { + writer.beginObject(); + if (debugMeta != null) { + writer.name(JsonKeys.DEBUG_META).value(logger, debugMeta); + } + writer.name(JsonKeys.PROFILER_ID).value(logger, profilerId); + writer.name(JsonKeys.CHUNK_ID).value(logger, chunkId); + if (clientSdk != null) { + writer.name(JsonKeys.CLIENT_SDK).value(logger, clientSdk); + } + if (!measurements.isEmpty()) { + writer.name(JsonKeys.MEASUREMENTS).value(logger, measurements); + } + writer.name(JsonKeys.PLATFORM).value(logger, platform); + writer.name(JsonKeys.RELEASE).value(logger, release); + if (environment != null) { + writer.name(JsonKeys.ENVIRONMENT).value(logger, environment); + } + writer.name(JsonKeys.VERSION).value(logger, version); + if (sampledProfile != null) { + writer.name(JsonKeys.SAMPLED_PROFILE).value(logger, sampledProfile); + } + if (unknown != null) { + for (String key : unknown.keySet()) { + Object value = unknown.get(key); + writer.name(key).value(logger, value); + } + } + writer.endObject(); + } + + @Nullable + @Override + public Map getUnknown() { + return unknown; + } + + @Override + public void setUnknown(@Nullable Map unknown) { + this.unknown = unknown; + } + + public static final class Deserializer implements JsonDeserializer { + + @Override + public @NotNull ProfileChunk deserialize(@NotNull ObjectReader reader, @NotNull ILogger logger) + throws Exception { + reader.beginObject(); + ProfileChunk data = new ProfileChunk(); + Map unknown = null; + + while (reader.peek() == JsonToken.NAME) { + final String nextName = reader.nextName(); + switch (nextName) { + case JsonKeys.DEBUG_META: + DebugMeta debugMeta = reader.nextOrNull(logger, new DebugMeta.Deserializer()); + if (debugMeta != null) { + data.debugMeta = debugMeta; + } + break; + case JsonKeys.PROFILER_ID: + SentryId profilerId = reader.nextOrNull(logger, new SentryId.Deserializer()); + if (profilerId != null) { + data.profilerId = profilerId; + } + break; + case JsonKeys.CHUNK_ID: + SentryId chunkId = reader.nextOrNull(logger, new SentryId.Deserializer()); + if (chunkId != null) { + data.chunkId = chunkId; + } + break; + case JsonKeys.CLIENT_SDK: + SdkVersion clientSdk = reader.nextOrNull(logger, new SdkVersion.Deserializer()); + if (clientSdk != null) { + data.clientSdk = clientSdk; + } + break; + case JsonKeys.MEASUREMENTS: + Map measurements = + reader.nextMapOrNull(logger, new ProfileMeasurement.Deserializer()); + if (measurements != null) { + data.measurements.putAll(measurements); + } + break; + case JsonKeys.PLATFORM: + String platform = reader.nextStringOrNull(); + if (platform != null) { + data.platform = platform; + } + break; + case JsonKeys.RELEASE: + String release = reader.nextStringOrNull(); + if (release != null) { + data.release = release; + } + break; + case JsonKeys.ENVIRONMENT: + String environment = reader.nextStringOrNull(); + if (environment != null) { + data.environment = environment; + } + break; + case JsonKeys.VERSION: + String version = reader.nextStringOrNull(); + if (version != null) { + data.version = version; + } + break; + case JsonKeys.SAMPLED_PROFILE: + String sampledProfile = reader.nextStringOrNull(); + if (sampledProfile != null) { + data.sampledProfile = sampledProfile; + } + break; + default: + if (unknown == null) { + unknown = new ConcurrentHashMap<>(); + } + reader.nextUnknown(logger, unknown, nextName); + break; + } + } + data.setUnknown(unknown); + reader.endObject(); + return data; + } + } +} diff --git a/sentry/src/main/java/io/sentry/ProfileContext.java b/sentry/src/main/java/io/sentry/ProfileContext.java new file mode 100644 index 0000000000..e4b411c279 --- /dev/null +++ b/sentry/src/main/java/io/sentry/ProfileContext.java @@ -0,0 +1,120 @@ +package io.sentry; + +import io.sentry.protocol.SentryId; +import io.sentry.util.CollectionUtils; +import io.sentry.util.Objects; +import io.sentry.vendor.gson.stream.JsonToken; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class ProfileContext implements JsonUnknown, JsonSerializable { + public static final String TYPE = "profile"; + + /** Determines which trace the Span belongs to. */ + private @NotNull SentryId profilerId; + + private @Nullable Map unknown; + + public ProfileContext() { + this(SentryId.EMPTY_ID); + } + + public ProfileContext(final @NotNull SentryId profilerId) { + this.profilerId = profilerId; + } + + /** + * Copy constructor. + * + * @param profileContext the ProfileContext to copy + */ + public ProfileContext(final @NotNull ProfileContext profileContext) { + this.profilerId = profileContext.profilerId; + final Map copiedUnknown = + CollectionUtils.newConcurrentHashMap(profileContext.unknown); + if (copiedUnknown != null) { + this.unknown = copiedUnknown; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ProfileContext)) return false; + ProfileContext that = (ProfileContext) o; + return profilerId.equals(that.profilerId); + } + + @Override + public int hashCode() { + return Objects.hash(profilerId); + } + + // region JsonSerializable + + public static final class JsonKeys { + public static final String PROFILER_ID = "profiler_id"; + } + + @Override + public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger) + throws IOException { + writer.beginObject(); + writer.name(JsonKeys.PROFILER_ID).value(logger, profilerId); + if (unknown != null) { + for (String key : unknown.keySet()) { + Object value = unknown.get(key); + writer.name(key).value(logger, value); + } + } + writer.endObject(); + } + + public @NotNull SentryId getProfilerId() { + return profilerId; + } + + @Nullable + @Override + public Map getUnknown() { + return unknown; + } + + @Override + public void setUnknown(@Nullable Map unknown) { + this.unknown = unknown; + } + + public static final class Deserializer implements JsonDeserializer { + @Override + public @NotNull ProfileContext deserialize( + @NotNull ObjectReader reader, @NotNull ILogger logger) throws Exception { + reader.beginObject(); + ProfileContext data = new ProfileContext(); + Map unknown = null; + while (reader.peek() == JsonToken.NAME) { + final String nextName = reader.nextName(); + switch (nextName) { + case JsonKeys.PROFILER_ID: + SentryId profilerId = reader.nextOrNull(logger, new SentryId.Deserializer()); + if (profilerId != null) { + data.profilerId = profilerId; + } + break; + default: + if (unknown == null) { + unknown = new ConcurrentHashMap<>(); + } + reader.nextUnknown(logger, unknown, nextName); + break; + } + } + data.setUnknown(unknown); + reader.endObject(); + return data; + } + } +} diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index 67b38f48bc..86f0f5f8bc 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -26,7 +26,7 @@ public final class Scopes implements IScopes { private final @Nullable Scopes parentScopes; private final @NotNull String creator; - private final @NotNull TransactionPerformanceCollector transactionPerformanceCollector; + private final @NotNull CompositePerformanceCollector compositePerformanceCollector; private final @NotNull CombinedScopeView combinedScope; @@ -53,7 +53,7 @@ private Scopes( final @NotNull SentryOptions options = getOptions(); validateOptions(options); - this.transactionPerformanceCollector = options.getTransactionPerformanceCollector(); + this.compositePerformanceCollector = options.getCompositePerformanceCollector(); } public @NotNull String getCreator() { @@ -404,7 +404,8 @@ public void close(final boolean isRestarting) { configureScope(scope -> scope.clear()); configureScope(ScopeType.ISOLATION, scope -> scope.clear()); getOptions().getTransactionProfiler().close(); - getOptions().getTransactionPerformanceCollector().close(); + getOptions().getContinuousProfiler().close(); + getOptions().getCompositePerformanceCollector().close(); final @NotNull ISentryExecutorService executorService = getOptions().getExecutorService(); if (isRestarting) { executorService.submit( @@ -809,6 +810,35 @@ public void flush(long timeoutMillis) { return sentryId; } + @ApiStatus.Internal + @Override + public @NotNull SentryId captureProfileChunk( + final @NotNull ProfileChunk profilingContinuousData) { + Objects.requireNonNull(profilingContinuousData, "profilingContinuousData is required"); + + @NotNull SentryId sentryId = SentryId.EMPTY_ID; + if (!isEnabled()) { + getOptions() + .getLogger() + .log( + SentryLevel.WARNING, + "Instance is disabled and this 'captureTransaction' call is a no-op."); + } else { + try { + sentryId = getClient().captureProfileChunk(profilingContinuousData, getScope()); + } catch (Throwable e) { + getOptions() + .getLogger() + .log( + SentryLevel.ERROR, + "Error while capturing profile chunk with id: " + + profilingContinuousData.getChunkId(), + e); + } + } + return sentryId; + } + @Override public @NotNull ITransaction startTransaction( final @NotNull TransactionContext transactionContext, @@ -868,10 +898,10 @@ public void flush(long timeoutMillis) { transaction = spanFactory.createTransaction( - transactionContext, this, transactionOptions, transactionPerformanceCollector); + transactionContext, this, transactionOptions, compositePerformanceCollector); // new SentryTracer( // transactionContext, this, transactionOptions, - // transactionPerformanceCollector); + // compositePerformanceCollector); // The listener is called only if the transaction exists, as the transaction is needed to // stop it @@ -893,6 +923,34 @@ public void flush(long timeoutMillis) { return transaction; } + @Override + public void startProfiler() { + if (getOptions().isContinuousProfilingEnabled()) { + getOptions().getLogger().log(SentryLevel.DEBUG, "Started continuous Profiling."); + getOptions().getContinuousProfiler().start(); + } else { + getOptions() + .getLogger() + .log( + SentryLevel.WARNING, + "Continuous Profiling is not enabled. Set profilesSampleRate and profilesSampler to null to enable it."); + } + } + + @Override + public void stopProfiler() { + if (getOptions().isContinuousProfilingEnabled()) { + getOptions().getLogger().log(SentryLevel.DEBUG, "Stopped continuous Profiling."); + getOptions().getContinuousProfiler().stop(); + } else { + getOptions() + .getLogger() + .log( + SentryLevel.WARNING, + "Continuous Profiling is not enabled. Set profilesSampleRate and profilesSampler to null to enable it."); + } + } + @Override @ApiStatus.Internal public void setSpanContext( diff --git a/sentry/src/main/java/io/sentry/ScopesAdapter.java b/sentry/src/main/java/io/sentry/ScopesAdapter.java index 6df6deee3d..9944a87a0a 100644 --- a/sentry/src/main/java/io/sentry/ScopesAdapter.java +++ b/sentry/src/main/java/io/sentry/ScopesAdapter.java @@ -268,6 +268,11 @@ public boolean isAncestorOf(final @Nullable IScopes otherScopes) { .captureTransaction(transaction, traceContext, hint, profilingTraceData); } + @Override + public @NotNull SentryId captureProfileChunk(@NotNull ProfileChunk profileChunk) { + return Sentry.getCurrentScopes().captureProfileChunk(profileChunk); + } + @Override public @NotNull ITransaction startTransaction( @NotNull TransactionContext transactionContext, @@ -275,6 +280,16 @@ public boolean isAncestorOf(final @Nullable IScopes otherScopes) { return Sentry.startTransaction(transactionContext, transactionOptions); } + @Override + public void startProfiler() { + Sentry.startProfiler(); + } + + @Override + public void stopProfiler() { + Sentry.stopProfiler(); + } + @ApiStatus.Internal @Override public void setSpanContext( diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 7a35720e2d..3c30597b92 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -1049,6 +1049,16 @@ public static void endSession() { return getCurrentScopes().startTransaction(transactionContext, transactionOptions); } + /** Starts the continuous profiler, if enabled. */ + public static void startProfiler() { + getCurrentScopes().startProfiler(); + } + + /** Starts the continuous profiler, if enabled. */ + public static void stopProfiler() { + getCurrentScopes().stopProfiler(); + } + /** * Gets the current active transaction or span. * diff --git a/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java b/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java index a9828792d7..b0926b9d93 100644 --- a/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java +++ b/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java @@ -18,6 +18,7 @@ public final class SentryAppStartProfilingOptions implements JsonUnknown, JsonSe @Nullable Double traceSampleRate; @Nullable String profilingTracesDirPath; boolean isProfilingEnabled; + boolean isContinuousProfilingEnabled; int profilingTracesHz; private @Nullable Map unknown; @@ -30,6 +31,7 @@ public SentryAppStartProfilingOptions() { profileSampleRate = null; profilingTracesDirPath = null; isProfilingEnabled = false; + isContinuousProfilingEnabled = false; profilingTracesHz = 0; } @@ -42,6 +44,7 @@ public SentryAppStartProfilingOptions() { profileSampleRate = samplingDecision.getProfileSampleRate(); profilingTracesDirPath = options.getProfilingTracesDirPath(); isProfilingEnabled = options.isProfilingEnabled(); + isContinuousProfilingEnabled = options.isContinuousProfilingEnabled(); profilingTracesHz = options.getProfilingTracesHz(); } @@ -93,6 +96,14 @@ public boolean isProfilingEnabled() { return isProfilingEnabled; } + public void setContinuousProfilingEnabled(final boolean continuousProfilingEnabled) { + isContinuousProfilingEnabled = continuousProfilingEnabled; + } + + public boolean isContinuousProfilingEnabled() { + return isContinuousProfilingEnabled; + } + public void setProfilingTracesHz(final int profilingTracesHz) { this.profilingTracesHz = profilingTracesHz; } @@ -110,6 +121,7 @@ public static final class JsonKeys { public static final String TRACE_SAMPLE_RATE = "trace_sample_rate"; public static final String PROFILING_TRACES_DIR_PATH = "profiling_traces_dir_path"; public static final String IS_PROFILING_ENABLED = "is_profiling_enabled"; + public static final String IS_CONTINUOUS_PROFILING_ENABLED = "is_continuous_profiling_enabled"; public static final String PROFILING_TRACES_HZ = "profiling_traces_hz"; } @@ -123,6 +135,9 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger writer.name(JsonKeys.TRACE_SAMPLE_RATE).value(logger, traceSampleRate); writer.name(JsonKeys.PROFILING_TRACES_DIR_PATH).value(logger, profilingTracesDirPath); writer.name(JsonKeys.IS_PROFILING_ENABLED).value(logger, isProfilingEnabled); + writer + .name(JsonKeys.IS_CONTINUOUS_PROFILING_ENABLED) + .value(logger, isContinuousProfilingEnabled); writer.name(JsonKeys.PROFILING_TRACES_HZ).value(logger, profilingTracesHz); if (unknown != null) { @@ -195,6 +210,12 @@ public static final class Deserializer options.isProfilingEnabled = isProfilingEnabled; } break; + case JsonKeys.IS_CONTINUOUS_PROFILING_ENABLED: + Boolean isContinuousProfilingEnabled = reader.nextBooleanOrNull(); + if (isContinuousProfilingEnabled != null) { + options.isContinuousProfilingEnabled = isContinuousProfilingEnabled; + } + break; case JsonKeys.PROFILING_TRACES_HZ: Integer profilingTracesHz = reader.nextIntegerOrNull(); if (profilingTracesHz != null) { diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index fdeb8b9b6b..e726e5d750 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -7,6 +7,7 @@ import io.sentry.hints.DiskFlushNotification; import io.sentry.hints.TransactionEnd; import io.sentry.protocol.Contexts; +import io.sentry.protocol.DebugMeta; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.transport.ITransport; @@ -463,8 +464,7 @@ private SentryEvent processEvent( return event; } - @Nullable - private SentryTransaction processTransaction( + private @Nullable SentryTransaction processTransaction( @NotNull SentryTransaction transaction, final @NotNull Hint hint, final @NotNull List eventProcessors) { @@ -853,6 +853,42 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint return sentryId; } + @ApiStatus.Internal + @Override + public @NotNull SentryId captureProfileChunk( + @NotNull ProfileChunk profileChunk, final @Nullable IScope scope) { + Objects.requireNonNull(profileChunk, "profileChunk is required."); + + options + .getLogger() + .log(SentryLevel.DEBUG, "Capturing profile chunk: %s", profileChunk.getChunkId()); + + @NotNull SentryId sentryId = profileChunk.getChunkId(); + final DebugMeta debugMeta = DebugMeta.buildDebugMeta(profileChunk.getDebugMeta(), options); + if (debugMeta != null) { + profileChunk.setDebugMeta(debugMeta); + } + + // BeforeSend and EventProcessors are not supported at the moment for Profile Chunks + + try { + final @NotNull SentryEnvelope envelope = + new SentryEnvelope( + new SentryEnvelopeHeader(sentryId, options.getSdkVersion(), null), + Collections.singletonList( + SentryEnvelopeItem.fromProfileChunk(profileChunk, options.getSerializer()))); + sentryId = sendEnvelope(envelope, null); + } catch (IOException | SentryEnvelopeException e) { + options + .getLogger() + .log(SentryLevel.WARNING, e, "Capturing profile chunk %s failed.", sentryId); + // if there was an error capturing the event, we return an emptyId + sentryId = SentryId.EMPTY_ID; + } + + return sentryId; + } + @Override @ApiStatus.Experimental public @NotNull SentryId captureCheckIn( diff --git a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java index 2d5f6484b3..62892e3ed4 100644 --- a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java +++ b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java @@ -34,6 +34,9 @@ @ApiStatus.Internal public final class SentryEnvelopeItem { + // Profiles bigger than 50 MB will be dropped by the backend, so we drop bigger ones + private static final long MAX_PROFILE_CHUNK_SIZE = 50 * 1024 * 1024; // 50MB + @SuppressWarnings("CharsetObjectCanBeUsed") private static final Charset UTF_8 = Charset.forName("UTF-8"); @@ -255,13 +258,64 @@ private static void ensureAttachmentSizeLimit( } } + public static @NotNull SentryEnvelopeItem fromProfileChunk( + final @NotNull ProfileChunk profileChunk, final @NotNull ISerializer serializer) + throws SentryEnvelopeException { + + final @NotNull File traceFile = profileChunk.getTraceFile(); + // Using CachedItem, so we read the trace file in the background + final CachedItem cachedItem = + new CachedItem( + () -> { + if (!traceFile.exists()) { + throw new SentryEnvelopeException( + String.format( + "Dropping profile chunk, because the file '%s' doesn't exists", + traceFile.getName())); + } + // The payload of the profile item is a json including the trace file encoded with + // base64 + final byte[] traceFileBytes = + readBytesFromFile(traceFile.getPath(), MAX_PROFILE_CHUNK_SIZE); + final @NotNull String base64Trace = + Base64.encodeToString(traceFileBytes, NO_WRAP | NO_PADDING); + if (base64Trace.isEmpty()) { + throw new SentryEnvelopeException("Profiling trace file is empty"); + } + profileChunk.setSampledProfile(base64Trace); + + try (final ByteArrayOutputStream stream = new ByteArrayOutputStream(); + final Writer writer = new BufferedWriter(new OutputStreamWriter(stream, UTF_8))) { + serializer.serialize(profileChunk, writer); + return stream.toByteArray(); + } catch (IOException e) { + throw new SentryEnvelopeException( + String.format("Failed to serialize profile chunk\n%s", e.getMessage())); + } finally { + // In any case we delete the trace file + traceFile.delete(); + } + }); + + SentryEnvelopeItemHeader itemHeader = + new SentryEnvelopeItemHeader( + SentryItemType.ProfileChunk, + () -> cachedItem.getBytes().length, + "application-json", + traceFile.getName()); + + // avoid method refs on Android due to some issues with older AGP setups + // noinspection Convert2MethodRef + return new SentryEnvelopeItem(itemHeader, () -> cachedItem.getBytes()); + } + public static @NotNull SentryEnvelopeItem fromProfilingTrace( final @NotNull ProfilingTraceData profilingTraceData, final long maxTraceFileSize, final @NotNull ISerializer serializer) throws SentryEnvelopeException { - File traceFile = profilingTraceData.getTraceFile(); + final @NotNull File traceFile = profilingTraceData.getTraceFile(); // Using CachedItem, so we read the trace file in the background final CachedItem cachedItem = new CachedItem( @@ -274,8 +328,10 @@ private static void ensureAttachmentSizeLimit( } // The payload of the profile item is a json including the trace file encoded with // base64 - byte[] traceFileBytes = readBytesFromFile(traceFile.getPath(), maxTraceFileSize); - String base64Trace = Base64.encodeToString(traceFileBytes, NO_WRAP | NO_PADDING); + final byte[] traceFileBytes = + readBytesFromFile(traceFile.getPath(), maxTraceFileSize); + final @NotNull String base64Trace = + Base64.encodeToString(traceFileBytes, NO_WRAP | NO_PADDING); if (base64Trace.isEmpty()) { throw new SentryEnvelopeException("Profiling trace file is empty"); } diff --git a/sentry/src/main/java/io/sentry/SentryItemType.java b/sentry/src/main/java/io/sentry/SentryItemType.java index 85acc0aadd..068c37ab1f 100644 --- a/sentry/src/main/java/io/sentry/SentryItemType.java +++ b/sentry/src/main/java/io/sentry/SentryItemType.java @@ -15,6 +15,7 @@ public enum SentryItemType implements JsonSerializable { Attachment("attachment"), Transaction("transaction"), Profile("profile"), + ProfileChunk("profile_chunk"), ClientReport("client_report"), ReplayEvent("replay_event"), ReplayRecording("replay_recording"), diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index c02765fd54..9c914eda4f 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -353,9 +353,12 @@ public class SentryOptions { /** Max trace file size in bytes. */ private long maxTraceFileSize = 5 * 1024 * 1024; - /** Listener interface to perform operations when a transaction is started or ended */ + /** Profiler that runs when a transaction is started until it's finished. */ private @NotNull ITransactionProfiler transactionProfiler = NoOpTransactionProfiler.getInstance(); + /** Profiler that runs continuously until stopped. */ + private @NotNull IContinuousProfiler continuousProfiler = NoOpContinuousProfiler.getInstance(); + /** * Contains a list of origins to which `sentry-trace` header should be sent in HTTP integrations. */ @@ -427,8 +430,8 @@ public class SentryOptions { private final @NotNull List performanceCollectors = new ArrayList<>(); /** Performance collector that collect performance stats while transactions run. */ - private @NotNull TransactionPerformanceCollector transactionPerformanceCollector = - NoOpTransactionPerformanceCollector.getInstance(); + private @NotNull CompositePerformanceCollector compositePerformanceCollector = + NoOpCompositePerformanceCollector.getInstance(); /** Enables the time-to-full-display spans in navigation transactions. */ private boolean enableTimeToFullDisplayTracing = false; @@ -1664,6 +1667,28 @@ public void setTransactionProfiler(final @Nullable ITransactionProfiler transact } } + /** + * Returns the continuous profiler. + * + * @return the continuous profiler. + */ + public @NotNull IContinuousProfiler getContinuousProfiler() { + return continuousProfiler; + } + + /** + * Sets the continuous profiler. It only has effect if no profiler was already set. + * + * @param continuousProfiler - the continuous profiler + */ + public void setContinuousProfiler(final @Nullable IContinuousProfiler continuousProfiler) { + // We allow to set the profiler only if it was not set before, and we don't allow to unset it. + if (this.continuousProfiler == NoOpContinuousProfiler.getInstance() + && continuousProfiler != null) { + this.continuousProfiler = continuousProfiler; + } + } + /** * Returns if profiling is enabled for transactions. * @@ -1674,6 +1699,17 @@ public boolean isProfilingEnabled() { || getProfilesSampler() != null; } + /** + * Returns if continuous profiling is enabled. This means that no profile sample rate has been + * set. + * + * @return if continuous profiling is enabled. + */ + @ApiStatus.Internal + public boolean isContinuousProfilingEnabled() { + return getProfilesSampleRate() == null && getProfilesSampler() == null; + } + /** * Returns the callback used to determine if a profile is sampled. * @@ -1997,24 +2033,24 @@ public void setThreadChecker(final @NotNull IThreadChecker threadChecker) { } /** - * Gets the performance collector used to collect performance stats while transactions run. + * Gets the performance collector used to collect performance stats in a time period. * * @return the performance collector. */ @ApiStatus.Internal - public @NotNull TransactionPerformanceCollector getTransactionPerformanceCollector() { - return transactionPerformanceCollector; + public @NotNull CompositePerformanceCollector getCompositePerformanceCollector() { + return compositePerformanceCollector; } /** - * Sets the performance collector used to collect performance stats while transactions run. + * Sets the performance collector used to collect performance stats in a time period. * - * @param transactionPerformanceCollector the performance collector. + * @param compositePerformanceCollector the performance collector. */ @ApiStatus.Internal - public void setTransactionPerformanceCollector( - final @NotNull TransactionPerformanceCollector transactionPerformanceCollector) { - this.transactionPerformanceCollector = transactionPerformanceCollector; + public void setCompositePerformanceCollector( + final @NotNull CompositePerformanceCollector compositePerformanceCollector) { + this.compositePerformanceCollector = compositePerformanceCollector; } /** diff --git a/sentry/src/main/java/io/sentry/SentryTracer.java b/sentry/src/main/java/io/sentry/SentryTracer.java index 65901a2e1a..943dc349f2 100644 --- a/sentry/src/main/java/io/sentry/SentryTracer.java +++ b/sentry/src/main/java/io/sentry/SentryTracer.java @@ -7,6 +7,7 @@ import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.Objects; import io.sentry.util.SpanUtils; +import io.sentry.util.thread.IThreadChecker; import java.util.ArrayList; import java.util.List; import java.util.ListIterator; @@ -50,7 +51,7 @@ public final class SentryTracer implements ITransaction { private @NotNull TransactionNameSource transactionNameSource; private final @NotNull Instrumenter instrumenter; private final @NotNull Contexts contexts = new Contexts(); - private final @Nullable TransactionPerformanceCollector transactionPerformanceCollector; + private final @Nullable CompositePerformanceCollector compositePerformanceCollector; private final @NotNull TransactionOptions transactionOptions; public SentryTracer(final @NotNull TransactionContext context, final @NotNull IScopes scopes) { @@ -68,7 +69,7 @@ public SentryTracer( final @NotNull TransactionContext context, final @NotNull IScopes scopes, final @NotNull TransactionOptions transactionOptions, - final @Nullable TransactionPerformanceCollector transactionPerformanceCollector) { + final @Nullable CompositePerformanceCollector compositePerformanceCollector) { Objects.requireNonNull(context, "context is required"); Objects.requireNonNull(scopes, "scopes are required"); @@ -77,7 +78,7 @@ public SentryTracer( this.name = context.getName(); this.instrumenter = context.getInstrumenter(); this.scopes = scopes; - this.transactionPerformanceCollector = transactionPerformanceCollector; + this.compositePerformanceCollector = compositePerformanceCollector; this.transactionNameSource = context.getTransactionNameSource(); this.transactionOptions = transactionOptions; @@ -87,10 +88,16 @@ public SentryTracer( this.baggage = new Baggage(scopes.getOptions().getLogger()); } + final @NotNull SentryId continuousProfilerId = + scopes.getOptions().getContinuousProfiler().getProfilerId(); + if (!continuousProfilerId.equals(SentryId.EMPTY_ID)) { + this.contexts.setProfile(new ProfileContext(continuousProfilerId)); + } + // We are currently sending the performance data only in profiles, but we are always sending // performance measurements. - if (transactionPerformanceCollector != null) { - transactionPerformanceCollector.start(this); + if (compositePerformanceCollector != null) { + compositePerformanceCollector.start(this); } if (transactionOptions.getIdleTimeout() != null @@ -220,8 +227,8 @@ public void finish( finishedCallback.execute(this); } - if (transactionPerformanceCollector != null) { - performanceCollectionData.set(transactionPerformanceCollector.stop(this)); + if (compositePerformanceCollector != null) { + performanceCollectionData.set(compositePerformanceCollector.stop(this)); } }); @@ -469,8 +476,8 @@ private ISpan createChild( spanContext, spanOptions, finishingSpan -> { - if (transactionPerformanceCollector != null) { - transactionPerformanceCollector.onSpanFinished(finishingSpan); + if (compositePerformanceCollector != null) { + compositePerformanceCollector.onSpanFinished(finishingSpan); } final FinishStatus finishStatus = this.finishStatus; if (transactionOptions.getIdleTimeout() != null) { @@ -495,8 +502,8 @@ private ISpan createChild( // timestamp, // spanOptions, // finishingSpan -> { - // if (transactionPerformanceCollector != null) { - // transactionPerformanceCollector.onSpanFinished(finishingSpan); + // if (compositePerformanceCollector != null) { + // compositePerformanceCollector.onSpanFinished(finishingSpan); // } // final FinishStatus finishStatus = this.finishStatus; // if (transactionOptions.getIdleTimeout() != null) { @@ -513,16 +520,17 @@ private ISpan createChild( // } // }); // span.setDescription(description); - final long threadId = scopes.getOptions().getThreadChecker().currentThreadSystemId(); - span.setData(SpanDataConvention.THREAD_ID, String.valueOf(threadId)); + final @NotNull IThreadChecker threadChecker = scopes.getOptions().getThreadChecker(); + final SentryId profilerId = scopes.getOptions().getContinuousProfiler().getProfilerId(); + if (!profilerId.equals(SentryId.EMPTY_ID)) { + span.setData(SpanDataConvention.PROFILER_ID, profilerId.toString()); + } span.setData( - SpanDataConvention.THREAD_NAME, - scopes.getOptions().getThreadChecker().isMainThread() - ? "main" - : Thread.currentThread().getName()); + SpanDataConvention.THREAD_ID, String.valueOf(threadChecker.currentThreadSystemId())); + span.setData(SpanDataConvention.THREAD_NAME, threadChecker.getCurrentThreadName()); this.children.add(span); - if (transactionPerformanceCollector != null) { - transactionPerformanceCollector.onSpanStarted(span); + if (compositePerformanceCollector != null) { + compositePerformanceCollector.onSpanStarted(span); } return span; } else { diff --git a/sentry/src/main/java/io/sentry/SpanContext.java b/sentry/src/main/java/io/sentry/SpanContext.java index 91e3abd956..0a9a143616 100644 --- a/sentry/src/main/java/io/sentry/SpanContext.java +++ b/sentry/src/main/java/io/sentry/SpanContext.java @@ -4,6 +4,7 @@ import io.sentry.protocol.SentryId; import io.sentry.util.CollectionUtils; import io.sentry.util.Objects; +import io.sentry.util.thread.IThreadChecker; import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; import java.util.Map; @@ -95,6 +96,11 @@ public SpanContext( this.description = description; this.status = status; this.origin = origin; + final IThreadChecker threadChecker = + ScopesAdapter.getInstance().getOptions().getThreadChecker(); + this.data.put( + SpanDataConvention.THREAD_ID, String.valueOf(threadChecker.currentThreadSystemId())); + this.data.put(SpanDataConvention.THREAD_NAME, threadChecker.getCurrentThreadName()); } /** @@ -110,10 +116,21 @@ public SpanContext(final @NotNull SpanContext spanContext) { this.op = spanContext.op; this.description = spanContext.description; this.status = spanContext.status; + this.origin = spanContext.origin; final Map copiedTags = CollectionUtils.newConcurrentHashMap(spanContext.tags); if (copiedTags != null) { this.tags = copiedTags; } + final Map copiedUnknown = + CollectionUtils.newConcurrentHashMap(spanContext.unknown); + if (copiedUnknown != null) { + this.unknown = copiedUnknown; + } + this.baggage = spanContext.baggage; + final Map copiedData = CollectionUtils.newConcurrentHashMap(spanContext.data); + if (copiedData != null) { + this.data = copiedData; + } } public void setOperation(final @NotNull String operation) { diff --git a/sentry/src/main/java/io/sentry/SpanDataConvention.java b/sentry/src/main/java/io/sentry/SpanDataConvention.java index ffe2414af3..c4329f6dca 100644 --- a/sentry/src/main/java/io/sentry/SpanDataConvention.java +++ b/sentry/src/main/java/io/sentry/SpanDataConvention.java @@ -25,4 +25,5 @@ public interface SpanDataConvention { String CONTRIBUTES_TTFD = "ui.contributes_to_ttfd"; String HTTP_START_TIMESTAMP = "http.start_timestamp"; String HTTP_END_TIMESTAMP = "http.end_timestamp"; + String PROFILER_ID = "profiler_id"; } diff --git a/sentry/src/main/java/io/sentry/profilemeasurements/ProfileMeasurementValue.java b/sentry/src/main/java/io/sentry/profilemeasurements/ProfileMeasurementValue.java index b0cebf5439..12972d36e4 100644 --- a/sentry/src/main/java/io/sentry/profilemeasurements/ProfileMeasurementValue.java +++ b/sentry/src/main/java/io/sentry/profilemeasurements/ProfileMeasurementValue.java @@ -1,14 +1,20 @@ package io.sentry.profilemeasurements; +import io.sentry.DateUtils; import io.sentry.ILogger; import io.sentry.JsonDeserializer; import io.sentry.JsonSerializable; import io.sentry.JsonUnknown; import io.sentry.ObjectReader; import io.sentry.ObjectWriter; +import io.sentry.SentryDate; +import io.sentry.SentryNanotimeDate; import io.sentry.util.Objects; import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Date; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.jetbrains.annotations.ApiStatus; @@ -19,16 +25,26 @@ public final class ProfileMeasurementValue implements JsonUnknown, JsonSerializable { private @Nullable Map unknown; + private @Nullable Double timestamp; private @NotNull String relativeStartNs; // timestamp in nanoseconds this frame was started private double value; // frame duration in nanoseconds + @SuppressWarnings("JavaUtilDate") public ProfileMeasurementValue() { - this(0L, 0); + this(0L, 0, new SentryNanotimeDate(new Date(0), 0)); } - public ProfileMeasurementValue(final @NotNull Long relativeStartNs, final @NotNull Number value) { + public ProfileMeasurementValue( + final @NotNull Long relativeStartNs, + final @NotNull Number value, + final @NotNull SentryDate timestamp) { this.relativeStartNs = relativeStartNs.toString(); this.value = value.doubleValue(); + this.timestamp = DateUtils.nanosToSeconds(timestamp.nanoTimestamp()); + } + + public @Nullable Double getTimestamp() { + return timestamp; } public double getValue() { @@ -46,7 +62,8 @@ public boolean equals(Object o) { ProfileMeasurementValue that = (ProfileMeasurementValue) o; return Objects.equals(unknown, that.unknown) && relativeStartNs.equals(that.relativeStartNs) - && value == that.value; + && value == that.value + && Objects.equals(timestamp, that.timestamp); } @Override @@ -59,6 +76,7 @@ public int hashCode() { public static final class JsonKeys { public static final String VALUE = "value"; public static final String START_NS = "elapsed_since_start_ns"; + public static final String TIMESTAMP = "timestamp"; } @Override @@ -67,6 +85,9 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger writer.beginObject(); writer.name(JsonKeys.VALUE).value(logger, value); writer.name(JsonKeys.START_NS).value(logger, relativeStartNs); + if (timestamp != null) { + writer.name(JsonKeys.TIMESTAMP).value(logger, doubleToBigDecimal(timestamp)); + } if (unknown != null) { for (String key : unknown.keySet()) { Object value = unknown.get(key); @@ -77,6 +98,10 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger writer.endObject(); } + private @NotNull BigDecimal doubleToBigDecimal(final @NotNull Double value) { + return BigDecimal.valueOf(value).setScale(6, RoundingMode.DOWN); + } + @Nullable @Override public Map getUnknown() { @@ -112,6 +137,18 @@ public static final class Deserializer implements JsonDeserializer(); diff --git a/sentry/src/main/java/io/sentry/protocol/Contexts.java b/sentry/src/main/java/io/sentry/protocol/Contexts.java index 53c97bcb0b..e9fd594bfd 100644 --- a/sentry/src/main/java/io/sentry/protocol/Contexts.java +++ b/sentry/src/main/java/io/sentry/protocol/Contexts.java @@ -7,6 +7,7 @@ import io.sentry.JsonSerializable; import io.sentry.ObjectReader; import io.sentry.ObjectWriter; +import io.sentry.ProfileContext; import io.sentry.SpanContext; import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.HintUtils; @@ -54,6 +55,8 @@ public Contexts(final @NotNull Contexts contexts) { this.setGpu(new Gpu((Gpu) value)); } else if (SpanContext.TYPE.equals(entry.getKey()) && value instanceof SpanContext) { this.setTrace(new SpanContext((SpanContext) value)); + } else if (ProfileContext.TYPE.equals(entry.getKey()) && value instanceof ProfileContext) { + this.setProfile(new ProfileContext((ProfileContext) value)); } else if (Response.TYPE.equals(entry.getKey()) && value instanceof Response) { this.setResponse(new Response((Response) value)); } else { @@ -77,6 +80,15 @@ public void setTrace(final @NotNull SpanContext traceContext) { this.put(SpanContext.TYPE, traceContext); } + public @Nullable ProfileContext getProfile() { + return toContextType(ProfileContext.TYPE, ProfileContext.class); + } + + public void setProfile(final @Nullable ProfileContext profileContext) { + Objects.requireNonNull(profileContext, "profileContext is required"); + this.put(ProfileContext.TYPE, profileContext); + } + public @Nullable App getApp() { return toContextType(App.TYPE, App.class); } @@ -263,6 +275,9 @@ public static final class Deserializer implements JsonDeserializer { case SpanContext.TYPE: contexts.setTrace(new SpanContext.Deserializer().deserialize(reader, logger)); break; + case ProfileContext.TYPE: + contexts.setProfile(new ProfileContext.Deserializer().deserialize(reader, logger)); + break; case Response.TYPE: contexts.setResponse(new Response.Deserializer().deserialize(reader, logger)); break; diff --git a/sentry/src/main/java/io/sentry/protocol/DebugMeta.java b/sentry/src/main/java/io/sentry/protocol/DebugMeta.java index 458c4de631..85ce67ab8e 100644 --- a/sentry/src/main/java/io/sentry/protocol/DebugMeta.java +++ b/sentry/src/main/java/io/sentry/protocol/DebugMeta.java @@ -6,12 +6,14 @@ import io.sentry.JsonUnknown; import io.sentry.ObjectReader; import io.sentry.ObjectWriter; +import io.sentry.SentryOptions; import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -51,6 +53,42 @@ public void setSdkInfo(final @Nullable SdkInfo sdkInfo) { this.sdkInfo = sdkInfo; } + @ApiStatus.Internal + public static @Nullable DebugMeta buildDebugMeta( + final @Nullable DebugMeta eventDebugMeta, final @NotNull SentryOptions options) { + final @NotNull List debugImages = new ArrayList<>(); + + if (options.getProguardUuid() != null) { + final DebugImage proguardMappingImage = new DebugImage(); + proguardMappingImage.setType(DebugImage.PROGUARD); + proguardMappingImage.setUuid(options.getProguardUuid()); + debugImages.add(proguardMappingImage); + } + + for (final @NotNull String bundleId : options.getBundleIds()) { + final DebugImage sourceBundleImage = new DebugImage(); + sourceBundleImage.setType(DebugImage.JVM); + sourceBundleImage.setDebugId(bundleId); + debugImages.add(sourceBundleImage); + } + + if (!debugImages.isEmpty()) { + DebugMeta debugMeta = eventDebugMeta; + + if (debugMeta == null) { + debugMeta = new DebugMeta(); + } + if (debugMeta.getImages() == null) { + debugMeta.setImages(debugImages); + } else { + debugMeta.getImages().addAll(debugImages); + } + + return debugMeta; + } + return null; + } + // JsonKeys public static final class JsonKeys { diff --git a/sentry/src/main/java/io/sentry/protocol/SentryTransaction.java b/sentry/src/main/java/io/sentry/protocol/SentryTransaction.java index 684001843a..d2a9e5140b 100644 --- a/sentry/src/main/java/io/sentry/protocol/SentryTransaction.java +++ b/sentry/src/main/java/io/sentry/protocol/SentryTransaction.java @@ -78,7 +78,7 @@ public SentryTransaction(final @NotNull SentryTracer sentryTracer) { final SpanContext tracerContext = sentryTracer.getSpanContext(); Map data = sentryTracer.getData(); // tags must be placed on the root of the transaction instead of contexts.trace.tags - final SpanContext tracerContextToSend = + final @NotNull SpanContext tracerContextToSend = new SpanContext( tracerContext.getTraceId(), tracerContext.getSpanId(), diff --git a/sentry/src/main/java/io/sentry/util/thread/IThreadChecker.java b/sentry/src/main/java/io/sentry/util/thread/IThreadChecker.java index 81af056e71..deea360f8c 100644 --- a/sentry/src/main/java/io/sentry/util/thread/IThreadChecker.java +++ b/sentry/src/main/java/io/sentry/util/thread/IThreadChecker.java @@ -32,6 +32,14 @@ public interface IThreadChecker { */ boolean isMainThread(final @NotNull SentryThread sentryThread); + /** + * Returns the name of the current thread + * + * @return the name of the current thread + */ + @NotNull + String getCurrentThreadName(); + /** * Returns the system id of the current thread. Currently only used for Android. * diff --git a/sentry/src/main/java/io/sentry/util/thread/NoOpThreadChecker.java b/sentry/src/main/java/io/sentry/util/thread/NoOpThreadChecker.java index b1497d17e7..f80a996785 100644 --- a/sentry/src/main/java/io/sentry/util/thread/NoOpThreadChecker.java +++ b/sentry/src/main/java/io/sentry/util/thread/NoOpThreadChecker.java @@ -33,6 +33,11 @@ public boolean isMainThread(@NotNull SentryThread sentryThread) { return false; } + @Override + public @NotNull String getCurrentThreadName() { + return ""; + } + @Override public long currentThreadSystemId() { return 0; diff --git a/sentry/src/main/java/io/sentry/util/thread/ThreadChecker.java b/sentry/src/main/java/io/sentry/util/thread/ThreadChecker.java index bfa8aac139..2f9b6fc1d2 100644 --- a/sentry/src/main/java/io/sentry/util/thread/ThreadChecker.java +++ b/sentry/src/main/java/io/sentry/util/thread/ThreadChecker.java @@ -44,6 +44,11 @@ public boolean isMainThread(final @NotNull SentryThread sentryThread) { return threadId != null && isMainThread(threadId); } + @Override + public @NotNull String getCurrentThreadName() { + return Thread.currentThread().getName(); + } + @Override public long currentThreadSystemId() { return Thread.currentThread().getId(); diff --git a/sentry/src/test/java/io/sentry/CheckInSerializationTest.kt b/sentry/src/test/java/io/sentry/CheckInSerializationTest.kt index db113fa009..20d6b69367 100644 --- a/sentry/src/test/java/io/sentry/CheckInSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/CheckInSerializationTest.kt @@ -28,7 +28,10 @@ class CheckInSerializationTest { it.traceId = SentryId("f382e3180c714217a81371f8c644aefe") it.spanId = SpanId("85694b9f567145a6") } - ) + ).apply { + data[SpanDataConvention.THREAD_ID] = 10 + data[SpanDataConvention.THREAD_NAME] = "test" + } ) duration = 12.3 environment = "env" diff --git a/sentry/src/test/java/io/sentry/DefaultTransactionPerformanceCollectorTest.kt b/sentry/src/test/java/io/sentry/DefaultCompositePerformanceCollectorTest.kt similarity index 83% rename from sentry/src/test/java/io/sentry/DefaultTransactionPerformanceCollectorTest.kt rename to sentry/src/test/java/io/sentry/DefaultCompositePerformanceCollectorTest.kt index 60005935c9..fe9dd6039d 100644 --- a/sentry/src/test/java/io/sentry/DefaultTransactionPerformanceCollectorTest.kt +++ b/sentry/src/test/java/io/sentry/DefaultCompositePerformanceCollectorTest.kt @@ -23,9 +23,9 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue -class DefaultTransactionPerformanceCollectorTest { +class DefaultCompositePerformanceCollectorTest { - private val className = "io.sentry.DefaultTransactionPerformanceCollector" + private val className = "io.sentry.DefaultCompositePerformanceCollector" private val ctorTypes: Array> = arrayOf(SentryOptions::class.java) private val fixture = Fixture() private val threadChecker = ThreadChecker.getInstance() @@ -33,6 +33,7 @@ class DefaultTransactionPerformanceCollectorTest { private class Fixture { lateinit var transaction1: ITransaction lateinit var transaction2: ITransaction + val id1 = "id1" val scopes: IScopes = mock() val options = SentryOptions() var mockTimer: Timer? = null @@ -50,7 +51,7 @@ class DefaultTransactionPerformanceCollectorTest { whenever(scopes.options).thenReturn(options) } - fun getSut(memoryCollector: IPerformanceSnapshotCollector? = JavaMemoryCollector(), cpuCollector: IPerformanceSnapshotCollector? = mockCpuCollector, executorService: ISentryExecutorService = deferredExecutorService): TransactionPerformanceCollector { + fun getSut(memoryCollector: IPerformanceSnapshotCollector? = JavaMemoryCollector(), cpuCollector: IPerformanceSnapshotCollector? = mockCpuCollector, executorService: ISentryExecutorService = deferredExecutorService): CompositePerformanceCollector { options.dsn = "https://key@sentry.io/proj" options.executorService = executorService if (cpuCollector != null) { @@ -61,7 +62,7 @@ class DefaultTransactionPerformanceCollectorTest { } transaction1 = SentryTracer(TransactionContext("", ""), scopes) transaction2 = SentryTracer(TransactionContext("", ""), scopes) - val collector = DefaultTransactionPerformanceCollector(options) + val collector = DefaultCompositePerformanceCollector(options) val timer: Timer = collector.getProperty("timer") ?: Timer(true) mockTimer = spy(timer) collector.injectForField("timer", mockTimer) @@ -104,6 +105,13 @@ class DefaultTransactionPerformanceCollectorTest { verify(fixture.mockTimer)!!.scheduleAtFixedRate(any(), any(), eq(100)) } + @Test + fun `when start with a string, timer is scheduled every 100 milliseconds`() { + val collector = fixture.getSut() + collector.start(fixture.id1) + verify(fixture.mockTimer)!!.scheduleAtFixedRate(any(), any(), eq(100)) + } + @Test fun `when stop, timer is stopped`() { val collector = fixture.getSut() @@ -113,6 +121,15 @@ class DefaultTransactionPerformanceCollectorTest { verify(fixture.mockTimer)!!.cancel() } + @Test + fun `when stop with a string, timer is stopped`() { + val collector = fixture.getSut() + collector.start(fixture.id1) + collector.stop(fixture.id1) + verify(fixture.mockTimer)!!.scheduleAtFixedRate(any(), any(), eq(100)) + verify(fixture.mockTimer)!!.cancel() + } + @Test fun `stopping a not collected transaction return null`() { val collector = fixture.getSut() @@ -122,34 +139,53 @@ class DefaultTransactionPerformanceCollectorTest { assertNull(data) } + @Test + fun `stopping a not collected id return null`() { + val collector = fixture.getSut() + val data = collector.stop(fixture.id1) + verify(fixture.mockTimer, never())!!.scheduleAtFixedRate(any(), any(), eq(100)) + verify(fixture.mockTimer, never())!!.cancel() + assertNull(data) + } + @Test fun `collector collect memory for multiple transactions`() { val collector = fixture.getSut() collector.start(fixture.transaction1) collector.start(fixture.transaction2) + collector.start(fixture.id1) // Let's sleep to make the collector get values Thread.sleep(300) val data1 = collector.stop(fixture.transaction1) - // There is still a transaction running: the timer shouldn't stop now + // There is still a transaction and an id running: the timer shouldn't stop now verify(fixture.mockTimer, never())!!.cancel() val data2 = collector.stop(fixture.transaction2) - // There are no more transactions running: the time should stop now + // There is still an id running: the timer shouldn't stop now + verify(fixture.mockTimer, never())!!.cancel() + + val data3 = collector.stop(fixture.id1) + // There are no more transactions or ids running: the time should stop now verify(fixture.mockTimer)!!.cancel() assertNotNull(data1) assertNotNull(data2) + assertNotNull(data3) val memoryData1 = data1.map { it.memoryData } val cpuData1 = data1.map { it.cpuData } val memoryData2 = data2.map { it.memoryData } val cpuData2 = data2.map { it.cpuData } + val memoryData3 = data3.map { it.memoryData } + val cpuData3 = data3.map { it.cpuData } // The data returned by the collector is not empty assertFalse(memoryData1.isEmpty()) assertFalse(cpuData1.isEmpty()) assertFalse(memoryData2.isEmpty()) assertFalse(cpuData2.isEmpty()) + assertFalse(memoryData3.isEmpty()) + assertFalse(cpuData3.isEmpty()) } @Test @@ -266,6 +302,27 @@ class DefaultTransactionPerformanceCollectorTest { verify(collector).clear() } + @Test + fun `Continuous collectors are not called when collecting using a string id`() { + val collector = mock() + fixture.options.performanceCollectors.add(collector) + val sut = fixture.getSut(memoryCollector = null, cpuCollector = null) + + // when a collection is started with an id + sut.start(fixture.id1) + + // collector should not be notified + verify(collector, never()).onSpanStarted(fixture.transaction1) + + // when the id collection is stopped + sut.stop(fixture.id1) + + // collector should not be notified + verify(collector, never()).onSpanFinished(fixture.transaction1) + + verify(collector).clear() + } + @Test fun `Continuous collectors are notified properly even when multiple txn are running`() { val collector = mock() diff --git a/sentry/src/test/java/io/sentry/HubAdapterTest.kt b/sentry/src/test/java/io/sentry/HubAdapterTest.kt index f57c45ba4e..310c153709 100644 --- a/sentry/src/test/java/io/sentry/HubAdapterTest.kt +++ b/sentry/src/test/java/io/sentry/HubAdapterTest.kt @@ -215,6 +215,12 @@ class HubAdapterTest { verify(scopes).captureTransaction(eq(transaction), eq(traceContext), eq(hint), eq(profilingTraceData)) } + @Test fun `captureProfileChunk calls Hub`() { + val profileChunk = mock() + HubAdapter.getInstance().captureProfileChunk(profileChunk) + verify(scopes).captureProfileChunk(eq(profileChunk)) + } + @Test fun `startTransaction calls Hub`() { val transactionContext = mock() val samplingContext = mock() @@ -259,4 +265,14 @@ class HubAdapterTest { HubAdapter.getInstance().reportFullyDisplayed() verify(scopes).reportFullyDisplayed() } + + @Test fun `startProfiler calls Hub`() { + HubAdapter.getInstance().startProfiler() + verify(scopes).startProfiler() + } + + @Test fun `stopProfiler calls Hub`() { + HubAdapter.getInstance().stopProfiler() + verify(scopes).stopProfiler() + } } diff --git a/sentry/src/test/java/io/sentry/JavaMemoryCollectorTest.kt b/sentry/src/test/java/io/sentry/JavaMemoryCollectorTest.kt index e2914edff6..7e076bba78 100644 --- a/sentry/src/test/java/io/sentry/JavaMemoryCollectorTest.kt +++ b/sentry/src/test/java/io/sentry/JavaMemoryCollectorTest.kt @@ -23,6 +23,6 @@ class JavaMemoryCollectorTest { assertNotNull(memoryData) assertEquals(-1, memoryData.usedNativeMemory) assertEquals(usedMemory, memoryData.usedHeapMemory) - assertNotEquals(0, memoryData.timestampMillis) + assertNotEquals(0, memoryData.timestamp.nanoTimestamp()) } } diff --git a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt index e9d12ff4b1..53dd7d85bf 100644 --- a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt +++ b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt @@ -28,8 +28,11 @@ import java.io.OutputStream import java.io.OutputStreamWriter import java.io.StringReader import java.io.StringWriter +import java.math.BigDecimal +import java.math.RoundingMode import java.nio.file.Files import java.util.Date +import java.util.HashMap import java.util.TimeZone import java.util.UUID import kotlin.test.BeforeTest @@ -500,10 +503,29 @@ class JsonSerializerTest { } } + @Test + fun `serializes profile context`() { + val profileContext = ProfileContext(SentryId("3367f5196c494acaae85bbbd535379ac")) + val expected = """{"profiler_id":"3367f5196c494acaae85bbbd535379ac"}""" + val json = serializeToString(profileContext) + assertEquals(expected, json) + } + + @Test + fun `deserializes profile context`() { + val json = """{"profiler_id":"3367f5196c494acaae85bbbd535379ac"}""" + val actual = fixture.serializer.deserialize(StringReader(json), ProfileContext::class.java) + assertNotNull(actual) { + assertEquals(SentryId("3367f5196c494acaae85bbbd535379ac"), it.profilerId) + } + } + @Test fun `serializes profilingTraceData`() { val profilingTraceData = ProfilingTraceData(fixture.traceFile, NoOpTransaction.getInstance()) val now = Date() + val measurementNow = SentryNanotimeDate() + val measurementNowSeconds = BigDecimal.valueOf(DateUtils.nanosToSeconds(measurementNow.nanoTimestamp())).setScale(6, RoundingMode.DOWN).toDouble() profilingTraceData.androidApiLevel = 21 profilingTraceData.deviceLocale = "deviceLocale" profilingTraceData.deviceManufacturer = "deviceManufacturer" @@ -533,22 +555,22 @@ class JsonSerializerTest { ProfileMeasurement.ID_SCREEN_FRAME_RATES to ProfileMeasurement( ProfileMeasurement.UNIT_HZ, - listOf(ProfileMeasurementValue(1, 60.1)) + listOf(ProfileMeasurementValue(1, 60.1, measurementNow)) ), ProfileMeasurement.ID_MEMORY_FOOTPRINT to ProfileMeasurement( ProfileMeasurement.UNIT_BYTES, - listOf(ProfileMeasurementValue(2, 100.52)) + listOf(ProfileMeasurementValue(2, 100.52, measurementNow)) ), ProfileMeasurement.ID_MEMORY_NATIVE_FOOTPRINT to ProfileMeasurement( ProfileMeasurement.UNIT_BYTES, - listOf(ProfileMeasurementValue(3, 104.52)) + listOf(ProfileMeasurementValue(3, 104.52, measurementNow)) ), ProfileMeasurement.ID_CPU_USAGE to ProfileMeasurement( ProfileMeasurement.UNIT_PERCENT, - listOf(ProfileMeasurementValue(5, 10.52)) + listOf(ProfileMeasurementValue(5, 10.52, measurementNow)) ) ) ) @@ -604,7 +626,8 @@ class JsonSerializerTest { "values" to listOf( mapOf( "value" to 60.1, - "elapsed_since_start_ns" to "1" + "elapsed_since_start_ns" to "1", + "timestamp" to measurementNowSeconds ) ) ), @@ -614,7 +637,8 @@ class JsonSerializerTest { "values" to listOf( mapOf( "value" to 100.52, - "elapsed_since_start_ns" to "2" + "elapsed_since_start_ns" to "2", + "timestamp" to measurementNowSeconds ) ) ), @@ -624,7 +648,8 @@ class JsonSerializerTest { "values" to listOf( mapOf( "value" to 104.52, - "elapsed_since_start_ns" to "3" + "elapsed_since_start_ns" to "3", + "timestamp" to measurementNowSeconds ) ) ), @@ -634,7 +659,8 @@ class JsonSerializerTest { "values" to listOf( mapOf( "value" to 10.52, - "elapsed_since_start_ns" to "5" + "elapsed_since_start_ns" to "5", + "timestamp" to measurementNowSeconds ) ) ) @@ -765,23 +791,23 @@ class JsonSerializerTest { val expectedMeasurements = mapOf( ProfileMeasurement.ID_SCREEN_FRAME_RATES to ProfileMeasurement( ProfileMeasurement.UNIT_HZ, - listOf(ProfileMeasurementValue(1, 60.1)) + listOf(ProfileMeasurementValue(1, 60.1, mock())) ), ProfileMeasurement.ID_FROZEN_FRAME_RENDERS to ProfileMeasurement( ProfileMeasurement.UNIT_NANOSECONDS, - listOf(ProfileMeasurementValue(2, 100)) + listOf(ProfileMeasurementValue(2, 100, mock())) ), ProfileMeasurement.ID_MEMORY_FOOTPRINT to ProfileMeasurement( ProfileMeasurement.UNIT_BYTES, - listOf(ProfileMeasurementValue(3, 1000)) + listOf(ProfileMeasurementValue(3, 1000, mock())) ), ProfileMeasurement.ID_MEMORY_NATIVE_FOOTPRINT to ProfileMeasurement( ProfileMeasurement.UNIT_BYTES, - listOf(ProfileMeasurementValue(4, 1100)) + listOf(ProfileMeasurementValue(4, 1100, mock())) ), ProfileMeasurement.ID_CPU_USAGE to ProfileMeasurement( ProfileMeasurement.UNIT_PERCENT, - listOf(ProfileMeasurementValue(5, 17.04)) + listOf(ProfileMeasurementValue(5, 17.04, mock())) ) ) assertEquals(expectedMeasurements, profilingTraceData.measurementsMap) @@ -798,10 +824,11 @@ class JsonSerializerTest { @Test fun `serializes profileMeasurement`() { - val measurementValues = listOf(ProfileMeasurementValue(1, 2), ProfileMeasurementValue(3, 4)) + val now = SentryNanotimeDate(Date(1), 1) + val measurementValues = listOf(ProfileMeasurementValue(1, 2, now), ProfileMeasurementValue(3, 4, now)) val profileMeasurement = ProfileMeasurement(ProfileMeasurement.UNIT_NANOSECONDS, measurementValues) val actual = serializeToString(profileMeasurement) - val expected = "{\"unit\":\"nanosecond\",\"values\":[{\"value\":2.0,\"elapsed_since_start_ns\":\"1\"},{\"value\":4.0,\"elapsed_since_start_ns\":\"3\"}]}" + val expected = "{\"unit\":\"nanosecond\",\"values\":[{\"value\":2.0,\"elapsed_since_start_ns\":\"1\",\"timestamp\":0.001000},{\"value\":4.0,\"elapsed_since_start_ns\":\"3\",\"timestamp\":0.001000}]}" assertEquals(expected, actual) } @@ -810,22 +837,22 @@ class JsonSerializerTest { val json = """{ "unit":"hz", "values":[ - {"value":"60.1","elapsed_since_start_ns":"1"},{"value":"100","elapsed_since_start_ns":"2"} + {"value":"60.1","elapsed_since_start_ns":"1"},{"value":"100","elapsed_since_start_ns":"2", "timestamp": 0.001} ] }""" val profileMeasurement = fixture.serializer.deserialize(StringReader(json), ProfileMeasurement::class.java) val expected = ProfileMeasurement( ProfileMeasurement.UNIT_HZ, - listOf(ProfileMeasurementValue(1, 60.1), ProfileMeasurementValue(2, 100)) + listOf(ProfileMeasurementValue(1, 60.1, SentryNanotimeDate(Date(0), 0)), ProfileMeasurementValue(2, 100, SentryNanotimeDate(Date(1), 1))) ) assertEquals(expected, profileMeasurement) } @Test fun `serializes profileMeasurementValue`() { - val profileMeasurementValue = ProfileMeasurementValue(1, 2) + val profileMeasurementValue = ProfileMeasurementValue(1, 2, SentryNanotimeDate(Date(1), 1)) val actual = serializeToString(profileMeasurementValue) - val expected = "{\"value\":2.0,\"elapsed_since_start_ns\":\"1\"}" + val expected = "{\"value\":2.0,\"elapsed_since_start_ns\":\"1\",\"timestamp\":0.001000}" assertEquals(expected, actual) } @@ -833,10 +860,205 @@ class JsonSerializerTest { fun `deserializes profileMeasurementValue`() { val json = """{"value":"60.1","elapsed_since_start_ns":"1"}""" val profileMeasurementValue = fixture.serializer.deserialize(StringReader(json), ProfileMeasurementValue::class.java) - val expected = ProfileMeasurementValue(1, 60.1) + val expected = ProfileMeasurementValue(1, 60.1, mock()) assertEquals(expected, profileMeasurementValue) assertEquals(60.1, profileMeasurementValue?.value) assertEquals("1", profileMeasurementValue?.relativeStartNs) + assertEquals(0.0, profileMeasurementValue?.timestamp) + } + + @Test + fun `deserializes profileMeasurementValue with timestamp`() { + val json = """{"value":"60.1","elapsed_since_start_ns":"1","timestamp":0.001000}""" + val profileMeasurementValue = fixture.serializer.deserialize(StringReader(json), ProfileMeasurementValue::class.java) + val expected = ProfileMeasurementValue(1, 60.1, SentryNanotimeDate(Date(1), 1)) + assertEquals(expected, profileMeasurementValue) + assertEquals(60.1, profileMeasurementValue?.value) + assertEquals("1", profileMeasurementValue?.relativeStartNs) + assertEquals(0.001, profileMeasurementValue?.timestamp) + } + + @Test + fun `serializes profileChunk`() { + val profilerId = SentryId() + val chunkId = SentryId() + fixture.options.sdkVersion = SdkVersion("test", "1.2.3") + fixture.options.release = "release" + fixture.options.environment = "environment" + val profileChunk = ProfileChunk(profilerId, chunkId, fixture.traceFile, HashMap(), fixture.options) + val measurementNow = SentryNanotimeDate() + val measurementNowSeconds = + BigDecimal.valueOf(DateUtils.nanosToSeconds(measurementNow.nanoTimestamp())).setScale(6, RoundingMode.DOWN) + .toDouble() + profileChunk.sampledProfile = "sampled profile in base 64" + profileChunk.measurements.putAll( + hashMapOf( + ProfileMeasurement.ID_SCREEN_FRAME_RATES to + ProfileMeasurement( + ProfileMeasurement.UNIT_HZ, + listOf(ProfileMeasurementValue(1, 60.1, measurementNow)) + ), + ProfileMeasurement.ID_MEMORY_FOOTPRINT to + ProfileMeasurement( + ProfileMeasurement.UNIT_BYTES, + listOf(ProfileMeasurementValue(2, 100.52, measurementNow)) + ), + ProfileMeasurement.ID_MEMORY_NATIVE_FOOTPRINT to + ProfileMeasurement( + ProfileMeasurement.UNIT_BYTES, + listOf(ProfileMeasurementValue(3, 104.52, measurementNow)) + ), + ProfileMeasurement.ID_CPU_USAGE to + ProfileMeasurement( + ProfileMeasurement.UNIT_PERCENT, + listOf(ProfileMeasurementValue(5, 10.52, measurementNow)) + ) + ) + ) + + val actual = serializeToString(profileChunk) + val reader = StringReader(actual) + val objectReader = JsonObjectReader(reader) + val element = JsonObjectDeserializer().deserialize(objectReader) as Map<*, *> + + assertEquals("android", element["platform"] as String) + assertEquals(profilerId.toString(), element["profiler_id"] as String) + assertEquals(chunkId.toString(), element["chunk_id"] as String) + assertEquals("environment", element["environment"] as String) + assertEquals("release", element["release"] as String) + assertEquals(mapOf("name" to "test", "version" to "1.2.3"), element["client_sdk"] as Map) + assertEquals("2", element["version"] as String) + assertEquals("sampled profile in base 64", element["sampled_profile"] as String) + assertEquals( + mapOf( + ProfileMeasurement.ID_SCREEN_FRAME_RATES to + mapOf( + "unit" to ProfileMeasurement.UNIT_HZ, + "values" to listOf( + mapOf( + "value" to 60.1, + "elapsed_since_start_ns" to "1", + "timestamp" to measurementNowSeconds + ) + ) + ), + ProfileMeasurement.ID_MEMORY_FOOTPRINT to + mapOf( + "unit" to ProfileMeasurement.UNIT_BYTES, + "values" to listOf( + mapOf( + "value" to 100.52, + "elapsed_since_start_ns" to "2", + "timestamp" to measurementNowSeconds + ) + ) + ), + ProfileMeasurement.ID_MEMORY_NATIVE_FOOTPRINT to + mapOf( + "unit" to ProfileMeasurement.UNIT_BYTES, + "values" to listOf( + mapOf( + "value" to 104.52, + "elapsed_since_start_ns" to "3", + "timestamp" to measurementNowSeconds + ) + ) + ), + ProfileMeasurement.ID_CPU_USAGE to + mapOf( + "unit" to ProfileMeasurement.UNIT_PERCENT, + "values" to listOf( + mapOf( + "value" to 10.52, + "elapsed_since_start_ns" to "5", + "timestamp" to measurementNowSeconds + ) + ) + ) + ), + element["measurements"] + ) + } + + @Test + fun `deserializes profileChunk`() { + val profilerId = SentryId() + val chunkId = SentryId() + val json = """{ + "client_sdk":{"name":"test","version":"1.2.3"}, + "chunk_id":"$chunkId", + "environment":"environment", + "platform":"android", + "profiler_id":"$profilerId", + "release":"release", + "sampled_profile":"sampled profile in base 64", + "version":"2", + "measurements":{ + "screen_frame_rates": { + "unit":"hz", + "values":[ + {"value":"60.1","elapsed_since_start_ns":"1"} + ] + }, + "frozen_frame_renders": { + "unit":"nanosecond", + "values":[ + {"value":"100","elapsed_since_start_ns":"2"} + ] + }, + "memory_footprint": { + "unit":"byte", + "values":[ + {"value":"1000","elapsed_since_start_ns":"3"} + ] + }, + "memory_native_footprint": { + "unit":"byte", + "values":[ + {"value":"1100","elapsed_since_start_ns":"4"} + ] + }, + "cpu_usage": { + "unit":"percent", + "values":[ + {"value":"17.04","elapsed_since_start_ns":"5"} + ] + } + } + }""" + val profileChunk = fixture.serializer.deserialize(StringReader(json), ProfileChunk::class.java) + assertNotNull(profileChunk) + assertEquals(SdkVersion("test", "1.2.3"), profileChunk.clientSdk) + assertEquals(chunkId, profileChunk.chunkId) + assertEquals("environment", profileChunk.environment) + assertEquals("android", profileChunk.platform) + assertEquals(profilerId, profileChunk.profilerId) + assertEquals("release", profileChunk.release) + assertEquals("sampled profile in base 64", profileChunk.sampledProfile) + assertEquals("2", profileChunk.version) + val expectedMeasurements = mapOf( + ProfileMeasurement.ID_SCREEN_FRAME_RATES to ProfileMeasurement( + ProfileMeasurement.UNIT_HZ, + listOf(ProfileMeasurementValue(1, 60.1, mock())) + ), + ProfileMeasurement.ID_FROZEN_FRAME_RENDERS to ProfileMeasurement( + ProfileMeasurement.UNIT_NANOSECONDS, + listOf(ProfileMeasurementValue(2, 100, mock())) + ), + ProfileMeasurement.ID_MEMORY_FOOTPRINT to ProfileMeasurement( + ProfileMeasurement.UNIT_BYTES, + listOf(ProfileMeasurementValue(3, 1000, mock())) + ), + ProfileMeasurement.ID_MEMORY_NATIVE_FOOTPRINT to ProfileMeasurement( + ProfileMeasurement.UNIT_BYTES, + listOf(ProfileMeasurementValue(4, 1100, mock())) + ), + ProfileMeasurement.ID_CPU_USAGE to ProfileMeasurement( + ProfileMeasurement.UNIT_PERCENT, + listOf(ProfileMeasurementValue(5, 17.04, mock())) + ) + ) + assertEquals(expectedMeasurements, profileChunk.measurements) } @Test @@ -846,6 +1068,7 @@ class JsonSerializerTest { trace.status = SpanStatus.OK trace.setTag("myTag", "myValue") trace.sampled = true + trace.data["dataKey"] = "dataValue" val tracer = SentryTracer(trace, fixture.scopes) tracer.setData("dataKey", "dataValue") val span = tracer.startChild("child") @@ -879,6 +1102,9 @@ class JsonSerializerTest { assertEquals("dataValue", (jsonTrace["data"] as Map<*, *>)["dataKey"] as String) assertNotNull(jsonTrace["trace_id"] as String) assertNotNull(jsonTrace["span_id"] as String) + assertNotNull(jsonTrace["data"] as Map<*, *>) { + assertEquals("dataValue", it["dataKey"]) + } assertEquals("http", jsonTrace["op"] as String) assertEquals("some request", jsonTrace["description"] as String) assertEquals("ok", jsonTrace["status"] as String) @@ -941,7 +1167,7 @@ class JsonSerializerTest { assertEquals("0a53026963414893", transaction.contexts.trace!!.spanId.toString()) assertEquals("http", transaction.contexts.trace!!.operation) assertNotNull(transaction.contexts["custom"]) - assertEquals("transactionDataValue", transaction.contexts.trace!!.data!!["transactionDataKey"]) + assertEquals("transactionDataValue", transaction.contexts.trace!!.data["transactionDataKey"]) assertEquals("some-value", (transaction.contexts["custom"] as Map<*, *>)["some-key"]) assertEquals("extraValue", transaction.getExtra("extraKey")) @@ -1004,7 +1230,8 @@ class JsonSerializerTest { val actual = serializeToString(appStartProfilingOptions) val expected = "{\"profile_sampled\":true,\"profile_sample_rate\":0.8,\"trace_sampled\":false," + - "\"trace_sample_rate\":0.1,\"profiling_traces_dir_path\":null,\"is_profiling_enabled\":false,\"profiling_traces_hz\":65}" + "\"trace_sample_rate\":0.1,\"profiling_traces_dir_path\":null,\"is_profiling_enabled\":false," + + "\"is_continuous_profiling_enabled\":false,\"profiling_traces_hz\":65}" assertEquals(expected, actual) } @@ -1021,6 +1248,7 @@ class JsonSerializerTest { assertEquals(appStartProfilingOptions.profileSampled, actual.profileSampled) assertEquals(appStartProfilingOptions.profileSampleRate, actual.profileSampleRate) assertEquals(appStartProfilingOptions.isProfilingEnabled, actual.isProfilingEnabled) + assertEquals(appStartProfilingOptions.isContinuousProfilingEnabled, actual.isContinuousProfilingEnabled) assertEquals(appStartProfilingOptions.profilingTracesHz, actual.profilingTracesHz) assertEquals(appStartProfilingOptions.profilingTracesDirPath, actual.profilingTracesDirPath) assertNull(actual.unknown) @@ -1324,6 +1552,7 @@ class JsonSerializerTest { profileSampled = true profileSampleRate = 0.8 isProfilingEnabled = false + isContinuousProfilingEnabled = false profilingTracesHz = 65 } diff --git a/sentry/src/test/java/io/sentry/NoOpContinuousProfilerTest.kt b/sentry/src/test/java/io/sentry/NoOpContinuousProfilerTest.kt index afbce4a8cb..e791651aef 100644 --- a/sentry/src/test/java/io/sentry/NoOpContinuousProfilerTest.kt +++ b/sentry/src/test/java/io/sentry/NoOpContinuousProfilerTest.kt @@ -1,6 +1,8 @@ package io.sentry +import io.sentry.protocol.SentryId import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertFalse class NoOpContinuousProfilerTest { @@ -22,4 +24,9 @@ class NoOpContinuousProfilerTest { @Test fun `close does not throw`() = profiler.close() + + @Test + fun `getProfilerId returns Empty SentryId`() { + assertEquals(profiler.profilerId, SentryId.EMPTY_ID) + } } diff --git a/sentry/src/test/java/io/sentry/NoOpHubTest.kt b/sentry/src/test/java/io/sentry/NoOpHubTest.kt index f20257482d..e0eb08ded0 100644 --- a/sentry/src/test/java/io/sentry/NoOpHubTest.kt +++ b/sentry/src/test/java/io/sentry/NoOpHubTest.kt @@ -31,6 +31,10 @@ class NoOpHubTest { fun `captureTransaction returns empty SentryId`() = assertEquals(SentryId.EMPTY_ID, sut.captureTransaction(mock(), mock())) + @Test + fun `captureProfileChunk returns empty SentryId`() = + assertEquals(SentryId.EMPTY_ID, sut.captureProfileChunk(mock())) + @Test fun `captureException returns empty SentryId`() = assertEquals(SentryId.EMPTY_ID, sut.captureException(RuntimeException())) @@ -111,4 +115,10 @@ class NoOpHubTest { sut.withScope(scopeCallback) verify(scopeCallback).run(NoOpScope.getInstance()) } + + @Test + fun `startProfiler doesnt throw`() = sut.startProfiler() + + @Test + fun `stopProfiler doesnt throw`() = sut.stopProfiler() } diff --git a/sentry/src/test/java/io/sentry/NoOpSentryClientTest.kt b/sentry/src/test/java/io/sentry/NoOpSentryClientTest.kt index 919ce5f083..8f8d76eba2 100644 --- a/sentry/src/test/java/io/sentry/NoOpSentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/NoOpSentryClientTest.kt @@ -63,6 +63,10 @@ class NoOpSentryClientTest { fun `captureTransaction returns empty SentryId`() = assertEquals(SentryId.EMPTY_ID, sut.captureTransaction(mock(), mock())) + @Test + fun `captureProfileChunk returns empty SentryId`() = + assertEquals(SentryId.EMPTY_ID, sut.captureProfileChunk(mock(), mock())) + @Test fun `captureCheckIn returns empty id`() { assertEquals(SentryId.EMPTY_ID, sut.captureCheckIn(mock(), mock(), mock())) diff --git a/sentry/src/test/java/io/sentry/OutboxSenderTest.kt b/sentry/src/test/java/io/sentry/OutboxSenderTest.kt index 8a1850e7dd..c8039551ff 100644 --- a/sentry/src/test/java/io/sentry/OutboxSenderTest.kt +++ b/sentry/src/test/java/io/sentry/OutboxSenderTest.kt @@ -38,6 +38,7 @@ class OutboxSenderTest { whenever(options.dsn).thenReturn("https://key@sentry.io/proj") whenever(options.dateProvider).thenReturn(SentryNanotimeDateProvider()) whenever(options.threadChecker).thenReturn(NoOpThreadChecker.getInstance()) + whenever(options.continuousProfiler).thenReturn(NoOpContinuousProfiler.getInstance()) whenever(scopes.options).thenReturn(this.options) } diff --git a/sentry/src/test/java/io/sentry/PerformanceCollectionDataTest.kt b/sentry/src/test/java/io/sentry/PerformanceCollectionDataTest.kt index e105e105c6..76866b09e6 100644 --- a/sentry/src/test/java/io/sentry/PerformanceCollectionDataTest.kt +++ b/sentry/src/test/java/io/sentry/PerformanceCollectionDataTest.kt @@ -1,5 +1,6 @@ package io.sentry +import org.mockito.kotlin.mock import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotEquals @@ -16,8 +17,10 @@ class PerformanceCollectionDataTest { @Test fun `only the last of multiple memory data is saved`() { val data = fixture.getSut() - val memData1 = MemoryCollectionData(0, 0, 0) - val memData2 = MemoryCollectionData(1, 1, 1) + val t1 = mock() + val t2 = mock() + val memData1 = MemoryCollectionData(0, 0, t1) + val memData2 = MemoryCollectionData(1, 1, t2) data.addMemoryData(memData1) data.addMemoryData(memData2) val savedMemoryData = data.memoryData @@ -28,8 +31,10 @@ class PerformanceCollectionDataTest { @Test fun `only the last of multiple cpu data is saved`() { val data = fixture.getSut() - val cpuData1 = CpuCollectionData(0, 0.0) - val cpuData2 = CpuCollectionData(1, 1.0) + val t1 = mock() + val t2 = mock() + val cpuData1 = CpuCollectionData(0.0, t1) + val cpuData2 = CpuCollectionData(1.0, t2) data.addCpuData(cpuData1) data.addCpuData(cpuData2) val savedCpuData = data.cpuData @@ -40,7 +45,7 @@ class PerformanceCollectionDataTest { @Test fun `null values are ignored`() { val data = fixture.getSut() - val cpuData1 = CpuCollectionData(0, 0.0) + val cpuData1 = CpuCollectionData(0.0, mock()) data.addCpuData(cpuData1) data.addCpuData(null) data.addMemoryData(null) diff --git a/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt b/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt index db2ed593ca..9c7418c6b5 100644 --- a/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt @@ -215,6 +215,12 @@ class ScopesAdapterTest { verify(scopes).captureTransaction(eq(transaction), eq(traceContext), eq(hint), eq(profilingTraceData)) } + @Test fun `captureProfileChunk calls Scopes`() { + val profileChunk = mock() + ScopesAdapter.getInstance().captureProfileChunk(profileChunk) + verify(scopes).captureProfileChunk(eq(profileChunk)) + } + @Test fun `startTransaction calls Scopes`() { val transactionContext = mock() val samplingContext = mock() @@ -259,4 +265,14 @@ class ScopesAdapterTest { ScopesAdapter.getInstance().reportFullyDisplayed() verify(scopes).reportFullyDisplayed() } + + @Test fun `startProfiler calls Scopes`() { + ScopesAdapter.getInstance().startProfiler() + verify(scopes).startProfiler() + } + + @Test fun `stopProfiler calls Scopes`() { + ScopesAdapter.getInstance().stopProfiler() + verify(scopes).stopProfiler() + } } diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index a806ca2175..68a1fee5d6 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -1576,6 +1576,52 @@ class ScopesTest { } //endregion + //region captureProfileChunk tests + @Test + fun `when captureProfileChunk is called on disabled client, do nothing`() { + val options = SentryOptions() + options.cacheDirPath = file.absolutePath + options.dsn = "https://key@sentry.io/proj" + options.setSerializer(mock()) + val sut = createScopes(options) + val mockClient = mock() + sut.bindClient(mockClient) + sut.close() + + sut.captureProfileChunk(mock()) + verify(mockClient, never()).captureProfileChunk(any(), any()) + verify(mockClient, never()).captureProfileChunk(any(), any()) + } + + @Test + fun `when captureProfileChunk, captureProfileChunk on the client should be called`() { + val options = SentryOptions() + options.cacheDirPath = file.absolutePath + options.dsn = "https://key@sentry.io/proj" + options.setSerializer(mock()) + val sut = createScopes(options) + val mockClient = createSentryClientMock() + sut.bindClient(mockClient) + + val profileChunk = mock() + sut.captureProfileChunk(profileChunk) + verify(mockClient).captureProfileChunk(eq(profileChunk), any()) + } + + @Test + fun `when profileChunk is called, lastEventId is not set`() { + val options = SentryOptions().apply { + dsn = "https://key@sentry.io/proj" + setSerializer(mock()) + } + val sut = createScopes(options) + val mockClient = createSentryClientMock() + sut.bindClient(mockClient) + sut.captureProfileChunk(mock()) + assertEquals(SentryId.EMPTY_ID, sut.lastEventId) + } + //endregion + //region profiling tests @Test @@ -1749,18 +1795,21 @@ class ScopesTest { fun `Scopes should close the sentry executor processor, profiler and performance collector on close call`() { val executor = mock() val profiler = mock() - val performanceCollector = mock() + val continuousProfiler = mock() + val performanceCollector = mock() val options = SentryOptions().apply { dsn = "https://key@sentry.io/proj" cacheDirPath = file.absolutePath executorService = executor setTransactionProfiler(profiler) - transactionPerformanceCollector = performanceCollector + compositePerformanceCollector = performanceCollector + setContinuousProfiler(continuousProfiler) } val sut = createScopes(options) sut.close() verify(executor).close(any()) verify(profiler).close() + verify(continuousProfiler).close() verify(performanceCollector).close() } @@ -2111,6 +2160,56 @@ class ScopesTest { assertEquals("other.span.origin", transaction.spanContext.origin) } + @Test + fun `startProfiler starts the continuous profiler`() { + val profiler = mock() + val scopes = generateScopes { + it.setContinuousProfiler(profiler) + } + scopes.startProfiler() + verify(profiler).start() + } + + @Test + fun `stopProfiler stops the continuous profiler`() { + val profiler = mock() + val scopes = generateScopes { + it.setContinuousProfiler(profiler) + } + scopes.stopProfiler() + verify(profiler).stop() + } + + @Test + fun `startProfiler logs instructions if continuous profiling is disabled`() { + val profiler = mock() + val logger = mock() + val scopes = generateScopes { + it.setContinuousProfiler(profiler) + it.profilesSampleRate = 1.0 + it.setLogger(logger) + it.isDebug = true + } + scopes.startProfiler() + verify(profiler, never()).start() + verify(logger).log(eq(SentryLevel.WARNING), eq("Continuous Profiling is not enabled. Set profilesSampleRate and profilesSampler to null to enable it.")) + } + + @Test + fun `stopProfiler logs instructions if continuous profiling is disabled`() { + val profiler = mock() + val logger = mock() + val scopes = generateScopes { + it.setContinuousProfiler(profiler) + it.profilesSampleRate = 1.0 + it.setLogger(logger) + it.isDebug = true + } + scopes.stopProfiler() + verify(profiler, never()).stop() + verify(logger).log(eq(SentryLevel.WARNING), eq("Continuous Profiling is not enabled. Set profilesSampleRate and profilesSampler to null to enable it.")) + } + private val dsnTest = "https://key@sentry.io/proj" private fun generateScopes(optionsConfiguration: Sentry.OptionsConfiguration? = null): IScopes { diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index a847f3a596..1e19cfb746 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -41,6 +41,7 @@ import org.mockito.kotlin.mockingDetails import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever import org.msgpack.core.MessagePack @@ -76,6 +77,8 @@ class SentryClientTest { val maxAttachmentSize: Long = (5 * 1024 * 1024).toLong() val scopes = mock() val sentryTracer: SentryTracer + val profileChunk: ProfileChunk + val profilingTraceFile = Files.createTempFile("trace", ".trace").toFile() var sentryOptions: SentryOptions = SentryOptions().apply { dsn = dsnString @@ -95,12 +98,12 @@ class SentryClientTest { whenever(scopes.options).thenReturn(sentryOptions) sentryTracer = SentryTracer(TransactionContext("a-transaction", "op", TracesSamplingDecision(true)), scopes) sentryTracer.startChild("a-span", "span 1").finish() + profileChunk = ProfileChunk(SentryId(), SentryId(), profilingTraceFile, emptyMap(), sentryOptions) } var attachment = Attachment("hello".toByteArray(), "hello.txt", "text/plain", true) var attachment2 = Attachment("hello2".toByteArray(), "hello2.txt", "text/plain", true) var attachment3 = Attachment("hello3".toByteArray(), "hello3.txt", "text/plain", true) - val profilingTraceFile = Files.createTempFile("trace", ".trace").toFile() var profilingTraceData = ProfilingTraceData(profilingTraceFile, sentryTracer) var profilingNonExistingTraceData = ProfilingTraceData(File("non_existent.trace"), sentryTracer) @@ -1083,6 +1086,22 @@ class SentryClientTest { ) } + @Test + fun `captureProfileChunk ignores beforeSend`() { + var invoked = false + fixture.sentryOptions.setBeforeSendTransaction { t, _ -> invoked = true; t } + fixture.getSut().captureProfileChunk(fixture.profileChunk, mock()) + assertFalse(invoked) + } + + @Test + fun `captureProfileChunk ignores Event Processors`() { + val mockProcessor = mock() + fixture.sentryOptions.addEventProcessor(mockProcessor) + fixture.getSut().captureProfileChunk(fixture.profileChunk, mock()) + verifyNoInteractions(mockProcessor) + } + @Test fun `when captureSession and no release is set, do nothing`() { fixture.getSut().captureSession(createSession("")) @@ -1483,6 +1502,29 @@ class SentryClientTest { assertFails { verifyProfilingTraceInEnvelope(SentryId(fixture.profilingNonExistingTraceData.profileId)) } } + @Test + fun `when captureProfileChunk`() { + val client = fixture.getSut() + client.captureProfileChunk(fixture.profileChunk, mock()) + verifyProfileChunkInEnvelope(fixture.profileChunk.chunkId) + } + + @Test + fun `when captureProfileChunk with empty trace file, profile chunk is not sent`() { + val client = fixture.getSut() + fixture.profilingTraceFile.writeText("") + client.captureProfileChunk(fixture.profileChunk, mock()) + assertFails { verifyProfilingTraceInEnvelope(fixture.profileChunk.chunkId) } + } + + @Test + fun `when captureProfileChunk with non existing profiling trace file, profile chunk is not sent`() { + val client = fixture.getSut() + fixture.profilingTraceFile.delete() + client.captureProfileChunk(fixture.profileChunk, mock()) + assertFails { verifyProfilingTraceInEnvelope(fixture.profileChunk.chunkId) } + } + @Test fun `when captureTransaction with attachments not added to transaction`() { val transaction = SentryTransaction(fixture.sentryTracer) @@ -3007,6 +3049,19 @@ class SentryClientTest { ) } + private fun verifyProfileChunkInEnvelope(eventId: SentryId?) { + verify(fixture.transport).send( + check { actual -> + assertEquals(eventId, actual.header.eventId) + + val profilingTraceItem = actual.items.firstOrNull { item -> + item.header.type == SentryItemType.ProfileChunk + } + assertNotNull(profilingTraceItem?.data) + } + ) + } + private class AbnormalHint(private val mechanism: String? = null) : AbnormalExit { override fun mechanism(): String? = mechanism override fun ignoreCurrentThread(): Boolean = false diff --git a/sentry/src/test/java/io/sentry/SentryEnvelopeItemTest.kt b/sentry/src/test/java/io/sentry/SentryEnvelopeItemTest.kt index 760d1270e5..a85e940e22 100644 --- a/sentry/src/test/java/io/sentry/SentryEnvelopeItemTest.kt +++ b/sentry/src/test/java/io/sentry/SentryEnvelopeItemTest.kt @@ -462,6 +462,94 @@ class SentryEnvelopeItemTest { ) } + @Test + fun `fromProfileChunk saves file as Base64`() { + val file = File(fixture.pathname) + val profileChunk = mock { + whenever(it.traceFile).thenReturn(file) + } + + file.writeBytes(fixture.bytes) + val chunk = SentryEnvelopeItem.fromProfileChunk(profileChunk, mock()).data + verify(profileChunk).sampledProfile = + Base64.encodeToString(fixture.bytes, Base64.NO_WRAP or Base64.NO_PADDING) + } + + @Test + fun `fromProfileChunk deletes file only after reading data`() { + val file = File(fixture.pathname) + val profileChunk = mock { + whenever(it.traceFile).thenReturn(file) + } + + file.writeBytes(fixture.bytes) + assert(file.exists()) + val chunk = SentryEnvelopeItem.fromProfileChunk(profileChunk, mock()) + assert(file.exists()) + chunk.data + assertFalse(file.exists()) + } + + @Test + fun `fromProfileChunk with invalid file throws`() { + val file = File(fixture.pathname) + val profileChunk = mock { + whenever(it.traceFile).thenReturn(file) + } + + assertFailsWith("Dropping profiling trace data, because the file ${file.path} doesn't exists") { + SentryEnvelopeItem.fromProfileChunk(profileChunk, mock()).data + } + } + + @Test + fun `fromProfileChunk with unreadable file throws`() { + val file = File(fixture.pathname) + val profileChunk = mock { + whenever(it.traceFile).thenReturn(file) + } + file.writeBytes(fixture.bytes) + file.setReadable(false) + assertFailsWith("Dropping profiling trace data, because the file ${file.path} doesn't exists") { + SentryEnvelopeItem.fromProfileChunk(profileChunk, mock()).data + } + } + + @Test + fun `fromProfileChunk with empty file throws`() { + val file = File(fixture.pathname) + file.writeBytes(ByteArray(0)) + val profileChunk = mock { + whenever(it.traceFile).thenReturn(file) + } + + val chunk = SentryEnvelopeItem.fromProfileChunk(profileChunk, mock()) + assertFailsWith("Profiling trace file is empty") { + chunk.data + } + } + + @Test + fun `fromProfileChunk with file too big`() { + val file = File(fixture.pathname) + val maxSize = 50 * 1024 * 1024 // 50MB + file.writeBytes(ByteArray((maxSize + 1)) { 0 }) + val profileChunk = mock { + whenever(it.traceFile).thenReturn(file) + } + + val exception = assertFailsWith { + SentryEnvelopeItem.fromProfileChunk(profileChunk, mock()).data + } + + assertEquals( + "Reading file failed, because size located at " + + "'${fixture.pathname}' with ${file.length()} bytes is bigger than the maximum " + + "allowed size of $maxSize bytes.", + exception.message + ) + } + @Test fun `fromReplay encodes payload into msgpack`() { val file = Files.createTempFile("replay", "").toFile() diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 21f33383e7..74ce882fe1 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -193,42 +193,47 @@ class SentryOptionsTest { } @Test - fun `when options is initialized, isProfilingEnabled is false`() { + fun `when options is initialized, isProfilingEnabled is false and isContinuousProfilingEnabled is true`() { assertFalse(SentryOptions().isProfilingEnabled) + assertTrue(SentryOptions().isContinuousProfilingEnabled) } @Test - fun `when profilesSampleRate is null and profilesSampler is null, isProfilingEnabled is false`() { + fun `when profilesSampleRate is null and profilesSampler is null, isProfilingEnabled is false and isContinuousProfilingEnabled is true`() { val options = SentryOptions().apply { this.profilesSampleRate = null this.profilesSampler = null } assertFalse(options.isProfilingEnabled) + assertTrue(options.isContinuousProfilingEnabled) } @Test - fun `when profilesSampleRate is 0 and profilesSampler is null, isProfilingEnabled is false`() { + fun `when profilesSampleRate is 0 and profilesSampler is null, isProfilingEnabled is false and isContinuousProfilingEnabled is false`() { val options = SentryOptions().apply { this.profilesSampleRate = 0.0 this.profilesSampler = null } assertFalse(options.isProfilingEnabled) + assertFalse(options.isContinuousProfilingEnabled) } @Test - fun `when profilesSampleRate is set to a value higher than 0, isProfilingEnabled is true`() { + fun `when profilesSampleRate is set to a value higher than 0, isProfilingEnabled is true and isContinuousProfilingEnabled is false`() { val options = SentryOptions().apply { this.profilesSampleRate = 0.1 } assertTrue(options.isProfilingEnabled) + assertFalse(options.isContinuousProfilingEnabled) } @Test - fun `when profilesSampler is set to a value, isProfilingEnabled is true`() { + fun `when profilesSampler is set to a value, isProfilingEnabled is true and isContinuousProfilingEnabled is false`() { val options = SentryOptions().apply { this.profilesSampler = SentryOptions.ProfilesSamplerCallback { 1.0 } } assertTrue(options.isProfilingEnabled) + assertFalse(options.isContinuousProfilingEnabled) } @Test @@ -250,8 +255,8 @@ class SentryOptionsTest { } @Test - fun `when options is initialized, transactionPerformanceCollector is set`() { - assertIs(SentryOptions().transactionPerformanceCollector) + fun `when options is initialized, compositePerformanceCollector is set`() { + assertIs(SentryOptions().compositePerformanceCollector) } @Test @@ -259,6 +264,11 @@ class SentryOptionsTest { assert(SentryOptions().transactionProfiler == NoOpTransactionProfiler.getInstance()) } + @Test + fun `when options is initialized, continuousProfiler is noop`() { + assert(SentryOptions().continuousProfiler == NoOpContinuousProfiler.getInstance()) + } + @Test fun `when options is initialized, collector is empty list`() { assertTrue(SentryOptions().performanceCollectors.isEmpty()) @@ -466,16 +476,16 @@ class SentryOptionsTest { } @Test - fun `when options are initialized, TransactionPerformanceCollector is a NoOp`() { - assertEquals(SentryOptions().transactionPerformanceCollector, NoOpTransactionPerformanceCollector.getInstance()) + fun `when options are initialized, CompositePerformanceCollector is a NoOp`() { + assertEquals(SentryOptions().compositePerformanceCollector, NoOpCompositePerformanceCollector.getInstance()) } @Test - fun `when setTransactionPerformanceCollector is called, overrides default`() { - val performanceCollector = mock() + fun `when setCompositePerformanceCollector is called, overrides default`() { + val performanceCollector = mock() val options = SentryOptions() - options.transactionPerformanceCollector = performanceCollector - assertEquals(performanceCollector, options.transactionPerformanceCollector) + options.compositePerformanceCollector = performanceCollector + assertEquals(performanceCollector, options.compositePerformanceCollector) } @Test diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index 5c707a9893..6f7cd427da 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -1277,6 +1277,52 @@ class SentryTest { assertNotSame(s1, s2) } + @Test + fun `startProfiler starts the continuous profiler`() { + val profiler = mock() + Sentry.init { + it.dsn = dsn + it.setContinuousProfiler(profiler) + } + Sentry.startProfiler() + verify(profiler).start() + } + + @Test + fun `startProfiler is ignored when continuous profiling is disabled`() { + val profiler = mock() + Sentry.init { + it.dsn = dsn + it.setContinuousProfiler(profiler) + it.profilesSampleRate = 1.0 + } + Sentry.startProfiler() + verify(profiler, never()).start() + } + + @Test + fun `stopProfiler stops the continuous profiler`() { + val profiler = mock() + Sentry.init { + it.dsn = dsn + it.setContinuousProfiler(profiler) + } + Sentry.stopProfiler() + verify(profiler).stop() + } + + @Test + fun `stopProfiler is ignored when continuous profiling is disabled`() { + val profiler = mock() + Sentry.init { + it.dsn = dsn + it.setContinuousProfiler(profiler) + it.profilesSampleRate = 1.0 + } + Sentry.stopProfiler() + verify(profiler, never()).stop() + } + private class InMemoryOptionsObserver : IOptionsObserver { var release: String? = null private set @@ -1328,6 +1374,7 @@ class SentryTest { override fun isMainThread(): Boolean = false override fun isMainThread(sentryThread: SentryThread): Boolean = false override fun currentThreadSystemId(): Long = 0 + override fun getCurrentThreadName(): String = "" } private class CustomMemoryCollector : diff --git a/sentry/src/test/java/io/sentry/SentryTracerTest.kt b/sentry/src/test/java/io/sentry/SentryTracerTest.kt index 3d5eca7f48..2be549cba6 100644 --- a/sentry/src/test/java/io/sentry/SentryTracerTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTracerTest.kt @@ -32,14 +32,18 @@ class SentryTracerTest { private class Fixture { val options = SentryOptions() val scopes: Scopes - val transactionPerformanceCollector: TransactionPerformanceCollector + val compositePerformanceCollector: CompositePerformanceCollector init { options.dsn = "https://key@sentry.io/proj" options.environment = "environment" options.release = "release@3.0.0" scopes = spy(createTestScopes(options)) - transactionPerformanceCollector = spy(DefaultTransactionPerformanceCollector(options)) + compositePerformanceCollector = spy( + DefaultCompositePerformanceCollector( + options + ) + ) } fun getSut( @@ -51,7 +55,7 @@ class SentryTracerTest { trimEnd: Boolean = false, transactionFinishedCallback: TransactionFinishedCallback? = null, samplingDecision: TracesSamplingDecision? = null, - performanceCollector: TransactionPerformanceCollector? = transactionPerformanceCollector + performanceCollector: CompositePerformanceCollector? = compositePerformanceCollector ): SentryTracer { optionsConfiguration.configure(options) @@ -209,6 +213,65 @@ class SentryTracerTest { verify(transactionProfiler).onTransactionFinish(any(), anyOrNull(), anyOrNull()) } + @Test + fun `when continuous profiler is running, profile context is set`() { + val continuousProfiler = mock() + val profilerId = SentryId() + whenever(continuousProfiler.profilerId).thenReturn(profilerId) + val tracer = fixture.getSut(optionsConfiguration = { + it.setContinuousProfiler(continuousProfiler) + }) + tracer.finish() + verify(fixture.scopes).captureTransaction( + check { + assertNotNull(it.contexts.profile) { + assertEquals(profilerId, it.profilerId) + } + }, + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + + @Test + fun `when continuous profiler is not running, profile context is not set`() { + val tracer = fixture.getSut(optionsConfiguration = { + it.setContinuousProfiler(NoOpContinuousProfiler.getInstance()) + }) + tracer.finish() + verify(fixture.scopes).captureTransaction( + check { + assertNull(it.contexts.profile) + }, + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + + @Test + fun `when continuous profiler is running, profiler id is set in span data`() { + val profilerId = SentryId() + val profiler = mock() + whenever(profiler.profilerId).thenReturn(profilerId) + + val tracer = fixture.getSut(optionsConfiguration = { options -> + options.setContinuousProfiler(profiler) + }) + val span = tracer.startChild("span.op") + assertEquals(profilerId.toString(), span.getData(SpanDataConvention.PROFILER_ID)) + } + + @Test + fun `when continuous profiler is not running, profiler id is not set in span data`() { + val tracer = fixture.getSut(optionsConfiguration = { options -> + options.setContinuousProfiler(NoOpContinuousProfiler.getInstance()) + }) + val span = tracer.startChild("span.op") + assertNull(span.getData(SpanDataConvention.PROFILER_ID)) + } + @Test fun `when transaction is finished, transaction is cleared from the scope`() { val tracer = fixture.getSut() @@ -1026,35 +1089,35 @@ class SentryTracerTest { } @Test - fun `when transaction is created, but not profiled, transactionPerformanceCollector is started anyway`() { + fun `when transaction is created, but not profiled, compositePerformanceCollector is started anyway`() { val transaction = fixture.getSut() - verify(fixture.transactionPerformanceCollector).start(anyOrNull()) + verify(fixture.compositePerformanceCollector).start(anyOrNull()) } @Test - fun `when transaction is created and profiled transactionPerformanceCollector is started`() { + fun `when transaction is created and profiled compositePerformanceCollector is started`() { val transaction = fixture.getSut(optionsConfiguration = { it.profilesSampleRate = 1.0 }, samplingDecision = TracesSamplingDecision(true, null, true, null)) - verify(fixture.transactionPerformanceCollector).start(check { assertEquals(transaction, it) }) + verify(fixture.compositePerformanceCollector).start(check { assertEquals(transaction, it) }) } @Test - fun `when transaction is finished, transactionPerformanceCollector is stopped`() { + fun `when transaction is finished, compositePerformanceCollector is stopped`() { val transaction = fixture.getSut() transaction.finish() - verify(fixture.transactionPerformanceCollector).stop(check { assertEquals(transaction, it) }) + verify(fixture.compositePerformanceCollector).stop(check { assertEquals(transaction, it) }) } @Test - fun `when a span is started and finished the transactionPerformanceCollector gets notified`() { + fun `when a span is started and finished the compositePerformanceCollector gets notified`() { val transaction = fixture.getSut() val span = transaction.startChild("op.span") span.finish() - verify(fixture.transactionPerformanceCollector).onSpanStarted(check { assertEquals(span, it) }) - verify(fixture.transactionPerformanceCollector).onSpanFinished(check { assertEquals(span, it) }) + verify(fixture.compositePerformanceCollector).onSpanStarted(check { assertEquals(span, it) }) + verify(fixture.compositePerformanceCollector).onSpanFinished(check { assertEquals(span, it) }) } @Test @@ -1208,11 +1271,13 @@ class SentryTracerTest { @Test fun `when transaction is finished, collected performance data is cleared`() { val data = mutableListOf(mock(), mock()) - val mockPerformanceCollector = object : TransactionPerformanceCollector { + val mockPerformanceCollector = object : CompositePerformanceCollector { override fun start(transaction: ITransaction) {} + override fun start(id: String) {} override fun onSpanStarted(span: ISpan) {} override fun onSpanFinished(span: ISpan) {} override fun stop(transaction: ITransaction): MutableList = data + override fun stop(id: String): MutableList = data override fun close() {} } val transaction = fixture.getSut(optionsConfiguration = { @@ -1363,6 +1428,7 @@ class SentryTracerTest { fun `when a span is launched on the main thread, the thread info should be set correctly`() { val threadChecker = mock() whenever(threadChecker.isMainThread).thenReturn(true) + whenever(threadChecker.currentThreadName).thenReturn("main") val tracer = fixture.getSut(optionsConfiguration = { options -> options.threadChecker = threadChecker @@ -1376,6 +1442,7 @@ class SentryTracerTest { fun `when a span is launched on the background thread, the thread info should be set correctly`() { val threadChecker = mock() whenever(threadChecker.isMainThread).thenReturn(false) + whenever(threadChecker.currentThreadName).thenReturn("test") val tracer = fixture.getSut(optionsConfiguration = { options -> options.threadChecker = threadChecker diff --git a/sentry/src/test/java/io/sentry/SpanContextTest.kt b/sentry/src/test/java/io/sentry/SpanContextTest.kt index 5e7ba9de25..47b98d5ee8 100644 --- a/sentry/src/test/java/io/sentry/SpanContextTest.kt +++ b/sentry/src/test/java/io/sentry/SpanContextTest.kt @@ -13,6 +13,13 @@ class SpanContextTest { assertNotNull(trace.spanId) } + @Test + fun `when created with default constructor, generates thread id and name`() { + val trace = SpanContext("op") + assertNotNull(trace.data[SpanDataConvention.THREAD_ID]) + assertNotNull(trace.data[SpanDataConvention.THREAD_NAME]) + } + @Test fun `sets tag`() { val trace = SpanContext("op") diff --git a/sentry/src/test/java/io/sentry/protocol/ContextsTest.kt b/sentry/src/test/java/io/sentry/protocol/ContextsTest.kt index 1d0573741f..b9c5d62eec 100644 --- a/sentry/src/test/java/io/sentry/protocol/ContextsTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/ContextsTest.kt @@ -1,5 +1,6 @@ package io.sentry.protocol +import io.sentry.ProfileContext import io.sentry.SpanContext import kotlin.test.Test import kotlin.test.assertEquals @@ -19,6 +20,7 @@ class ContextsTest { contexts.setGpu(Gpu()) contexts.setResponse(Response()) contexts.setTrace(SpanContext("op")) + contexts.profile = ProfileContext(SentryId()) val clone = Contexts(contexts) @@ -31,15 +33,18 @@ class ContextsTest { assertNotSame(contexts.runtime, clone.runtime) assertNotSame(contexts.gpu, clone.gpu) assertNotSame(contexts.trace, clone.trace) + assertNotSame(contexts.profile, clone.profile) assertNotSame(contexts.response, clone.response) } @Test fun `copying contexts will have the same values`() { val contexts = Contexts() + val id = SentryId() contexts["some-property"] = "some-value" contexts.setTrace(SpanContext("op")) contexts.trace!!.description = "desc" + contexts.profile = ProfileContext(id) val clone = Contexts(contexts) @@ -47,5 +52,6 @@ class ContextsTest { assertNotSame(contexts, clone) assertEquals(contexts["some-property"], clone["some-property"]) assertEquals(contexts.trace!!.description, clone.trace!!.description) + assertEquals(contexts.profile!!.profilerId, clone.profile!!.profilerId) } } diff --git a/sentry/src/test/java/io/sentry/protocol/DebugMetaTest.kt b/sentry/src/test/java/io/sentry/protocol/DebugMetaTest.kt index 17544a300f..21395dfc5c 100644 --- a/sentry/src/test/java/io/sentry/protocol/DebugMetaTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/DebugMetaTest.kt @@ -1,5 +1,6 @@ package io.sentry.protocol +import io.sentry.SentryOptions import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -16,4 +17,75 @@ class DebugMetaTest { assertEquals(3, it.size) } } + + @Test + fun `when event does not have debug meta and proguard uuids are set, attaches debug information`() { + val options = SentryOptions().apply { proguardUuid = "id1" } + val debugMeta = DebugMeta.buildDebugMeta(null, options) + + assertNotNull(debugMeta) { + assertNotNull(it.images) { images -> + assertEquals("id1", images[0].uuid) + assertEquals("proguard", images[0].type) + } + } + } + + @Test + fun `when event does not have debug meta and bundle ids are set, attaches debug information`() { + val options = SentryOptions().apply { bundleIds.addAll(listOf("id1", "id2")) } + val debugMeta = DebugMeta.buildDebugMeta(null, options) + + assertNotNull(debugMeta) { + assertNotNull(it.images) { images -> + assertEquals("id1", images[0].debugId) + assertEquals("jvm", images[0].type) + assertEquals("id2", images[1].debugId) + assertEquals("jvm", images[1].type) + } + } + } + + @Test + fun `when event has debug meta and proguard uuids are set, attaches debug information`() { + val options = SentryOptions().apply { proguardUuid = "id1" } + val debugMeta = DebugMeta.buildDebugMeta(DebugMeta(), options) + + assertNotNull(debugMeta) { + assertNotNull(it.images) { images -> + assertEquals("id1", images[0].uuid) + assertEquals("proguard", images[0].type) + } + } + } + + @Test + fun `when event has debug meta and bundle ids are set, attaches debug information`() { + val options = SentryOptions().apply { bundleIds.addAll(listOf("id1", "id2")) } + val debugMeta = DebugMeta.buildDebugMeta(DebugMeta(), options) + + assertNotNull(debugMeta) { + assertNotNull(it.images) { images -> + assertEquals("id1", images[0].debugId) + assertEquals("jvm", images[0].type) + assertEquals("id2", images[1].debugId) + assertEquals("jvm", images[1].type) + } + } + } + + @Test + fun `when event has debug meta as well as images and bundle ids are set, attaches debug information`() { + val options = SentryOptions().apply { bundleIds.addAll(listOf("id1", "id2")) } + val debugMeta = DebugMeta.buildDebugMeta(DebugMeta().also { it.images = listOf() }, options) + + assertNotNull(debugMeta) { + assertNotNull(it.images) { images -> + assertEquals("id1", images[0].debugId) + assertEquals("jvm", images[0].type) + assertEquals("id2", images[1].debugId) + assertEquals("jvm", images[1].type) + } + } + } } diff --git a/sentry/src/test/java/io/sentry/protocol/SpanContextSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/SpanContextSerializationTest.kt index 707daa78f1..2ebc830a5e 100644 --- a/sentry/src/test/java/io/sentry/protocol/SpanContextSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/SpanContextSerializationTest.kt @@ -6,6 +6,7 @@ import io.sentry.JsonObjectReader import io.sentry.JsonObjectWriter import io.sentry.JsonSerializable import io.sentry.SpanContext +import io.sentry.SpanDataConvention import io.sentry.SpanId import io.sentry.SpanStatus import io.sentry.TracesSamplingDecision @@ -35,6 +36,8 @@ class SpanContextSerializationTest { setTag("2a5fa3f5-7b87-487f-aaa5-84567aa73642", "4781d51a-c5af-47f2-a4ed-f030c9b3e194") setTag("29106d7d-7fa4-444f-9d34-b9d7510c69ab", "218c23ea-694a-497e-bf6d-e5f26f1ad7bd") setTag("ba9ce913-269f-4c03-882d-8ca5e6991b14", "35a74e90-8db8-4610-a411-872cbc1030ac") + data[SpanDataConvention.THREAD_NAME] = "test" + data[SpanDataConvention.THREAD_ID] = 10 setData("spanContextDataKey", "spanContextDataValue") } } diff --git a/sentry/src/test/java/io/sentry/util/thread/ThreadCheckerTest.kt b/sentry/src/test/java/io/sentry/util/thread/ThreadCheckerTest.kt index 26de021fbd..12b1e34827 100644 --- a/sentry/src/test/java/io/sentry/util/thread/ThreadCheckerTest.kt +++ b/sentry/src/test/java/io/sentry/util/thread/ThreadCheckerTest.kt @@ -2,6 +2,7 @@ package io.sentry.util.thread import io.sentry.protocol.SentryThread import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -43,4 +44,11 @@ class ThreadCheckerTest { } assertFalse(threadChecker.isMainThread(sentryThread)) } + + @Test + fun `currentThreadName returns the name of the current thread`() { + val thread = Thread.currentThread() + thread.name = "test" + assertEquals("test", threadChecker.currentThreadName) + } } diff --git a/sentry/src/test/resources/json/checkin_crontab.json b/sentry/src/test/resources/json/checkin_crontab.json index 8c39685878..c2bff2a050 100644 --- a/sentry/src/test/resources/json/checkin_crontab.json +++ b/sentry/src/test/resources/json/checkin_crontab.json @@ -25,7 +25,12 @@ "trace_id": "f382e3180c714217a81371f8c644aefe", "span_id": "85694b9f567145a6", "op": "default", - "origin": "manual" + "origin": "manual", + "data": + { + "thread.name": "test", + "thread.id": 10 + } } } } diff --git a/sentry/src/test/resources/json/checkin_interval.json b/sentry/src/test/resources/json/checkin_interval.json index 8281ca67ab..395bb03bba 100644 --- a/sentry/src/test/resources/json/checkin_interval.json +++ b/sentry/src/test/resources/json/checkin_interval.json @@ -26,7 +26,12 @@ "trace_id": "f382e3180c714217a81371f8c644aefe", "span_id": "85694b9f567145a6", "op": "default", - "origin": "manual" + "origin": "manual", + "data": + { + "thread.name": "test", + "thread.id": 10 + } } } } diff --git a/sentry/src/test/resources/json/contexts.json b/sentry/src/test/resources/json/contexts.json index a6f35b31a6..e8df6a21c1 100644 --- a/sentry/src/test/resources/json/contexts.json +++ b/sentry/src/test/resources/json/contexts.json @@ -123,7 +123,9 @@ }, "data": { - "spanContextDataKey": "spanContextDataValue" + "spanContextDataKey": "spanContextDataValue", + "thread.name": "test", + "thread.id": 10 } } } diff --git a/sentry/src/test/resources/json/sentry_base_event.json b/sentry/src/test/resources/json/sentry_base_event.json index d2d1fd0088..63ae8f03cf 100644 --- a/sentry/src/test/resources/json/sentry_base_event.json +++ b/sentry/src/test/resources/json/sentry_base_event.json @@ -126,7 +126,9 @@ }, "data": { - "spanContextDataKey": "spanContextDataValue" + "spanContextDataKey": "spanContextDataValue", + "thread.name": "test", + "thread.id": 10 } } }, diff --git a/sentry/src/test/resources/json/sentry_base_event_with_null_extra.json b/sentry/src/test/resources/json/sentry_base_event_with_null_extra.json index 4ce74eaf09..2079b424cb 100644 --- a/sentry/src/test/resources/json/sentry_base_event_with_null_extra.json +++ b/sentry/src/test/resources/json/sentry_base_event_with_null_extra.json @@ -126,7 +126,9 @@ }, "data": { - "spanContextDataKey": "spanContextDataValue" + "spanContextDataKey": "spanContextDataValue", + "thread.name": "test", + "thread.id": 10 } } }, diff --git a/sentry/src/test/resources/json/sentry_event.json b/sentry/src/test/resources/json/sentry_event.json index 6d421fc993..c6f8dd68b0 100644 --- a/sentry/src/test/resources/json/sentry_event.json +++ b/sentry/src/test/resources/json/sentry_event.json @@ -261,7 +261,9 @@ }, "data": { - "spanContextDataKey": "spanContextDataValue" + "spanContextDataKey": "spanContextDataValue", + "thread.name": "test", + "thread.id": 10 } } }, diff --git a/sentry/src/test/resources/json/sentry_replay_event.json b/sentry/src/test/resources/json/sentry_replay_event.json index d3970bf5b0..7bd64037d7 100644 --- a/sentry/src/test/resources/json/sentry_replay_event.json +++ b/sentry/src/test/resources/json/sentry_replay_event.json @@ -144,7 +144,9 @@ }, "data": { - "spanContextDataKey": "spanContextDataValue" + "spanContextDataKey": "spanContextDataValue", + "thread.name": "test", + "thread.id": 10 } } }, diff --git a/sentry/src/test/resources/json/sentry_transaction.json b/sentry/src/test/resources/json/sentry_transaction.json index 33080c9686..daa6d025e9 100644 --- a/sentry/src/test/resources/json/sentry_transaction.json +++ b/sentry/src/test/resources/json/sentry_transaction.json @@ -183,7 +183,9 @@ }, "data": { - "spanContextDataKey": "spanContextDataValue" + "spanContextDataKey": "spanContextDataValue", + "thread.name": "test", + "thread.id": 10 } } }, diff --git a/sentry/src/test/resources/json/sentry_transaction_legacy_date_format.json b/sentry/src/test/resources/json/sentry_transaction_legacy_date_format.json index 0d6ed5eb09..316b44bbaa 100644 --- a/sentry/src/test/resources/json/sentry_transaction_legacy_date_format.json +++ b/sentry/src/test/resources/json/sentry_transaction_legacy_date_format.json @@ -183,7 +183,9 @@ }, "data": { - "spanContextDataKey": "spanContextDataValue" + "spanContextDataKey": "spanContextDataValue", + "thread.name": "test", + "thread.id": 10 } } }, diff --git a/sentry/src/test/resources/json/sentry_transaction_no_measurement_unit.json b/sentry/src/test/resources/json/sentry_transaction_no_measurement_unit.json index 2d965be1b9..cf927b322b 100644 --- a/sentry/src/test/resources/json/sentry_transaction_no_measurement_unit.json +++ b/sentry/src/test/resources/json/sentry_transaction_no_measurement_unit.json @@ -153,7 +153,9 @@ }, "data": { - "spanContextDataKey": "spanContextDataValue" + "spanContextDataKey": "spanContextDataValue", + "thread.name": "test", + "thread.id": 10 } } }, diff --git a/sentry/src/test/resources/json/span_context.json b/sentry/src/test/resources/json/span_context.json index c55841a391..edff574fa4 100644 --- a/sentry/src/test/resources/json/span_context.json +++ b/sentry/src/test/resources/json/span_context.json @@ -14,6 +14,8 @@ }, "data": { - "spanContextDataKey": "spanContextDataValue" + "spanContextDataKey": "spanContextDataValue", + "thread.name": "test", + "thread.id": 10 } }