diff --git a/CHANGELOG.md b/CHANGELOG.md index bd1ff31566c..6a687fab8ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,50 @@ ### Features - Add native stack frame address information and debug image metadata to ANR events ([#4061](https://github.com/getsentry/sentry-java/pull/4061)) - - This enables symbolication for stripped native code in ANRs + - This enables symbolication for stripped native code in ANRs +- Add Continuous Profiling Support ([#3710](https://github.com/getsentry/sentry-java/pull/3710)) + + To enable Continuous Profiling use the `Sentry.startProfileSession` and `Sentry.stopProfileSession` experimental APIs. Sampling rate can be set through `options.profileSessionSampleRate`, which defaults to null (disabled). + Note: Both `options.profilesSampler` and `options.profilesSampleRate` must **not** be set to enable Continuous Profiling. + + ```java + import io.sentry.ProfileLifecycle; + import io.sentry.android.core.SentryAndroid; + + SentryAndroid.init(context) { options -> + + // Currently under experimental options: + options.getExperimental().setProfileSessionSampleRate(1.0); + // In manual mode, you need to start and stop the profiler manually using Sentry.startProfileSession and Sentry.stopProfileSession + // In trace mode, the profiler will start and stop automatically whenever a sampled trace starts and finishes + options.getExperimental().setProfileLifecycle(ProfileLifecycle.MANUAL); + } + // Start profiling + Sentry.startProfileSession(); + + // After all profiling is done, stop the profiler. Profiles can last indefinitely if not stopped. + Sentry.stopProfileSession(); + ``` + ```kotlin + import io.sentry.ProfileLifecycle + import io.sentry.android.core.SentryAndroid + + SentryAndroid.init(context) { options -> + + // Currently under experimental options: + options.experimental.profileSessionSampleRate = 1.0 + // In manual mode, you need to start and stop the profiler manually using Sentry.startProfileSession and Sentry.stopProfileSession + // In trace mode, the profiler will start and stop automatically whenever a sampled trace starts and finishes + options.experimental.profileLifecycle = ProfileLifecycle.MANUAL + } + // Start profiling + Sentry.startProfileSession() + + // After all profiling is done, stop the profiler. Profiles can last indefinitely if not stopped. + Sentry.stopProfileSession() + ``` + + To learn more visit [Sentry's Continuous Profiling](https://docs.sentry.io/product/explore/profiling/transaction-vs-continuous-profiling/#continuous-profiling-mode) documentation page. ### Fixes diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37f853b1c84..38d67ceb108 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,3 +1,4 @@ +#Mon Mar 17 13:40:54 CET 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 6e42257b576..18bb7b58ee7 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -40,6 +40,18 @@ public final class io/sentry/android/core/ActivityLifecycleIntegration : android public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V } +public class io/sentry/android/core/AndroidContinuousProfiler : io/sentry/IContinuousProfiler, io/sentry/transport/RateLimiter$IRateLimitObserver { + 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 getRootSpanCounter ()I + public fun isRunning ()Z + public fun onRateLimitChanged (Lio/sentry/transport/RateLimiter;)V + public fun reevaluateSampling ()V + public fun startProfileSession (Lio/sentry/ProfileLifecycle;Lio/sentry/TracesSampler;)V + public fun stopProfileSession (Lio/sentry/ProfileLifecycle;)V +} + public final class io/sentry/android/core/AndroidCpuCollector : io/sentry/IPerformanceSnapshotCollector { public fun (Lio/sentry/ILogger;)V public fun collect (Lio/sentry/PerformanceCollectionData;)V @@ -460,6 +472,7 @@ public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/andr public fun clear ()V public fun createProcessInitSpan ()Lio/sentry/android/core/performance/TimeSpan; 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; @@ -481,6 +494,7 @@ public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/andr public static fun onContentProviderPostCreate (Landroid/content/ContentProvider;)V public fun registerLifecycleCallbacks (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 new file mode 100644 index 00000000000..632a6a4bf22 --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java @@ -0,0 +1,373 @@ +package io.sentry.android.core; + +import static io.sentry.DataCategory.All; +import static io.sentry.IConnectionStatusProvider.ConnectionStatus.DISCONNECTED; +import static java.util.concurrent.TimeUnit.SECONDS; + +import android.os.Build; +import io.sentry.CompositePerformanceCollector; +import io.sentry.DataCategory; +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.ProfileLifecycle; +import io.sentry.Sentry; +import io.sentry.SentryDate; +import io.sentry.SentryLevel; +import io.sentry.SentryNanotimeDate; +import io.sentry.SentryOptions; +import io.sentry.TracesSampler; +import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; +import io.sentry.protocol.SentryId; +import io.sentry.transport.RateLimiter; +import io.sentry.util.SentryRandom; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.VisibleForTesting; + +@ApiStatus.Internal +public class AndroidContinuousProfiler + implements IContinuousProfiler, RateLimiter.IRateLimitObserver { + private static final long MAX_CHUNK_DURATION_MILLIS = 60000; + + private final @NotNull ILogger logger; + private final @Nullable String profilingTracesDirPath; + private final int profilingTracesHz; + private final @NotNull ISentryExecutorService executorService; + private final @NotNull BuildInfoProvider buildInfoProvider; + private boolean isInitialized = false; + private final @NotNull SentryFrameMetricsCollector frameMetricsCollector; + private @Nullable AndroidProfiler profiler = null; + private boolean isRunning = false; + private @Nullable IScopes scopes; + 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; + private final @NotNull AtomicBoolean isClosed = new AtomicBoolean(false); + private @NotNull SentryDate startProfileChunkTimestamp = new SentryNanotimeDate(); + private boolean shouldSample = true; + private boolean isSampled = false; + private int rootSpanCounter = 0; + + public AndroidContinuousProfiler( + final @NotNull BuildInfoProvider buildInfoProvider, + final @NotNull SentryFrameMetricsCollector frameMetricsCollector, + final @NotNull ILogger logger, + final @Nullable String profilingTracesDirPath, + final int profilingTracesHz, + final @NotNull ISentryExecutorService executorService) { + this.logger = logger; + this.frameMetricsCollector = frameMetricsCollector; + this.buildInfoProvider = buildInfoProvider; + this.profilingTracesDirPath = profilingTracesDirPath; + this.profilingTracesHz = profilingTracesHz; + this.executorService = executorService; + } + + private void init() { + // We initialize it only once + if (isInitialized) { + return; + } + isInitialized = true; + if (profilingTracesDirPath == null) { + logger.log( + SentryLevel.WARNING, + "Disabling profiling because no profiling traces dir path is defined in options."); + return; + } + if (profilingTracesHz <= 0) { + logger.log( + SentryLevel.WARNING, + "Disabling profiling because trace rate is set to %d", + profilingTracesHz); + return; + } + + profiler = + new AndroidProfiler( + profilingTracesDirPath, + (int) SECONDS.toMicros(1) / profilingTracesHz, + frameMetricsCollector, + null, + logger); + } + + @Override + public synchronized void startProfileSession( + final @NotNull ProfileLifecycle profileLifecycle, + final @NotNull TracesSampler tracesSampler) { + if (shouldSample) { + isSampled = tracesSampler.sampleSessionProfile(SentryRandom.current().nextDouble()); + shouldSample = false; + } + if (!isSampled) { + logger.log(SentryLevel.DEBUG, "Profiler was not started due to sampling decision."); + return; + } + switch (profileLifecycle) { + case TRACE: + // rootSpanCounter should never be negative, unless the user changed profile lifecycle while + // the profiler is running or close() is called. This is just a safety check. + if (rootSpanCounter < 0) { + rootSpanCounter = 0; + } + rootSpanCounter++; + break; + case MANUAL: + // We check if the profiler is already running and log a message only in manual mode, since + // in trace mode we can have multiple concurrent traces + if (isRunning()) { + logger.log(SentryLevel.DEBUG, "Profiler is already running."); + return; + } + break; + } + if (!isRunning()) { + logger.log(SentryLevel.DEBUG, "Started Profiler."); + start(); + } + } + + private synchronized void start() { + if ((scopes == null || scopes == NoOpScopes.getInstance()) + && Sentry.getCurrentScopes() != NoOpScopes.getInstance()) { + this.scopes = Sentry.getCurrentScopes(); + this.performanceCollector = + Sentry.getCurrentScopes().getOptions().getCompositePerformanceCollector(); + final @Nullable RateLimiter rateLimiter = scopes.getRateLimiter(); + if (rateLimiter != null) { + rateLimiter.addRateLimitObserver(this); + } + } + + // 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; + + // Let's initialize trace folder and profiling interval + init(); + // init() didn't create profiler, should never happen + if (profiler == null) { + return; + } + + if (scopes != null) { + final @Nullable RateLimiter rateLimiter = scopes.getRateLimiter(); + if (rateLimiter != null + && (rateLimiter.isActiveForCategory(All) + || rateLimiter.isActiveForCategory(DataCategory.ProfileChunk))) { + logger.log(SentryLevel.WARNING, "SDK is rate limited. Stopping profiler."); + // Let's stop and reset profiler id, as the profile is now broken anyway + stop(false); + return; + } + + // If device is offline, we don't start the profiler, to avoid flooding the cache + if (scopes.getOptions().getConnectionStatusProvider().getConnectionStatus() == DISCONNECTED) { + logger.log(SentryLevel.WARNING, "Device is offline. Stopping profiler."); + // Let's stop and reset profiler id, as the profile is now broken anyway + stop(false); + return; + } + startProfileChunkTimestamp = scopes.getOptions().getDateProvider().now(); + } else { + startProfileChunkTimestamp = new SentryNanotimeDate(); + } + final AndroidProfiler.ProfileStartData startData = profiler.start(); + // check if profiling started + 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 { + stopFuture = executorService.schedule(() -> stop(true), MAX_CHUNK_DURATION_MILLIS); + } catch (RejectedExecutionException e) { + logger.log( + SentryLevel.ERROR, + "Failed to schedule profiling chunk finish. Did you call Sentry.close()?", + e); + } + } + + @Override + public synchronized void stopProfileSession(final @NotNull ProfileLifecycle profileLifecycle) { + switch (profileLifecycle) { + case TRACE: + rootSpanCounter--; + // If there are active spans, and profile lifecycle is trace, we don't stop the profiler + if (rootSpanCounter > 0) { + return; + } + // rootSpanCounter should never be negative, unless the user changed profile lifecycle while + // the profiler is running or close() is called. This is just a safety check. + if (rootSpanCounter < 0) { + rootSpanCounter = 0; + } + stop(false); + break; + case MANUAL: + stop(false); + break; + } + } + + private synchronized void stop(final boolean restartProfiler) { + if (stopFuture != null) { + stopFuture.cancel(true); + } + // check if profiler was created and it's running + if (profiler == null || !isRunning) { + // When the profiler is stopped due to an error (e.g. offline or rate limited), reset the ids + profilerId = SentryId.EMPTY_ID; + chunkId = SentryId.EMPTY_ID; + return; + } + + // onTransactionStart() is only available since Lollipop_MR1 + // and SystemClock.elapsedRealtimeNanos() since Jelly Bean + if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) { + return; + } + + 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) { + 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, + startProfileChunkTimestamp)); + } + } + + isRunning = false; + // A chunk is finished. Next chunk will have a different id. + chunkId = SentryId.EMPTY_ID; + + 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."); + } + } + + public synchronized void reevaluateSampling() { + shouldSample = true; + } + + public synchronized void close() { + rootSpanCounter = 0; + stop(false); + isClosed.set(true); + } + + @Override + public @NotNull SentryId getProfilerId() { + return profilerId; + } + + private void sendChunks(final @NotNull IScopes scopes, final @NotNull SentryOptions options) { + try { + options + .getExecutorService() + .submit( + () -> { + // SDK is closed, we don't send the chunks + if (isClosed.get()) { + return; + } + 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; + } + + @VisibleForTesting + @Nullable + Future getStopFuture() { + return stopFuture; + } + + @VisibleForTesting + public int getRootSpanCounter() { + return rootSpanCounter; + } + + @Override + public void onRateLimitChanged(@NotNull RateLimiter rateLimiter) { + // We stop the profiler as soon as we are rate limited, to avoid the performance overhead + if (rateLimiter.isActiveForCategory(All) + || rateLimiter.isActiveForCategory(DataCategory.ProfileChunk)) { + logger.log(SentryLevel.WARNING, "SDK is rate limited. Stopping profiler."); + stop(false); + } + // If we are not rate limited anymore, we don't do anything: the profile is broken, so it's + // useless to restart it automatically + } +} 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 ccd878761db..800d4826fed 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 f475c1801ba..41c2d2da037 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 90b6c5d741e..6465381a8ea 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; @@ -165,23 +168,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())); @@ -229,7 +232,57 @@ static void initializeIntegrationsAndProcessors( "options.getFrameMetricsCollector is required"))); } } - options.setTransactionPerformanceCollector(new DefaultTransactionPerformanceCollector(options)); + options.setCompositePerformanceCollector(new DefaultCompositePerformanceCollector(options)); + } + + /** 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( 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 c1ae1237e23..e51e06fc6e0 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; @@ -93,7 +95,7 @@ public ProfileEndData( private final @NotNull ArrayDeque frozenFrameRenderMeasurements = new ArrayDeque<>(); private final @NotNull Map measurementsMap = new HashMap<>(); - private final @NotNull ISentryExecutorService executorService; + private final @Nullable ISentryExecutorService timeoutExecutorService; private final @NotNull ILogger logger; private boolean isRunning = false; protected final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); @@ -102,13 +104,14 @@ public AndroidProfiler( final @NotNull String tracesFilesDirPath, final int intervalUs, final @NotNull SentryFrameMetricsCollector frameMetricsCollector, - final @NotNull ISentryExecutorService executorService, + final @Nullable ISentryExecutorService timeoutExecutorService, final @NotNull ILogger logger) { this.traceFilesDir = new File(Objects.requireNonNull(tracesFilesDirPath, "TracesFilesDirPath is required")); this.intervalUs = intervalUs; this.logger = Objects.requireNonNull(logger, "Logger is required"); - this.executorService = Objects.requireNonNull(executorService, "ExecutorService is required."); + // Timeout executor is nullable, as timeouts will not be there for continuous profiling + this.timeoutExecutorService = timeoutExecutorService; this.frameMetricsCollector = Objects.requireNonNull(frameMetricsCollector, "SentryFrameMetricsCollector is required"); } @@ -153,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() @@ -166,23 +170,29 @@ 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)); } } }); // We stop profiling after a timeout to avoid huge profiles to be sent try { - scheduledFinish = - executorService.schedule(() -> endAndCollect(true, null), PROFILING_TIMEOUT_MILLIS); + if (timeoutExecutorService != null) { + scheduledFinish = + timeoutExecutorService.schedule( + () -> endAndCollect(true, null), PROFILING_TIMEOUT_MILLIS); + } } catch (RejectedExecutionException e) { logger.log( SentryLevel.ERROR, @@ -227,8 +237,7 @@ public void onFrameMetricCollected( try { // If there is any problem with the file this method could throw, but the start is also // wrapped, so this should never happen (except for tests, where this is the only method - // that - // throws) + // that throws) Debug.stopMethodTracing(); } catch (Throwable e) { logger.log(SentryLevel.ERROR, "Error while stopping profiling: ", e); @@ -315,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/AndroidTransactionProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java index 13ffe0bd66c..0aa678d5195 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java @@ -205,6 +205,7 @@ public void bindTransaction(final @NotNull ITransaction transaction) { // onTransactionStart() is only available since Lollipop_MR1 // and SystemClock.elapsedRealtimeNanos() since Jelly Bean + // and SUPPORTED_ABIS since KITKAT if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) return null; // Transaction finished, but it's not in the current profile diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java index 71c6894045e..e1f97aa78be 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java @@ -5,6 +5,7 @@ import android.os.Bundle; import io.sentry.ILogger; import io.sentry.InitPriority; +import io.sentry.ProfileLifecycle; import io.sentry.SentryIntegrationPackageStorage; import io.sentry.SentryLevel; import io.sentry.protocol.SdkVersion; @@ -63,6 +64,13 @@ final class ManifestMetadataReader { static final String PROFILES_SAMPLE_RATE = "io.sentry.traces.profiling.sample-rate"; + static final String PROFILE_SESSION_SAMPLE_RATE = + "io.sentry.traces.profiling.session-sample-rate"; + + static final String PROFILE_LIFECYCLE = "io.sentry.traces.profiling.lifecycle"; + + static final String PROFILER_START_ON_APP_START = "io.sentry.traces.profiling.start-on-app-start"; + @ApiStatus.Experimental static final String TRACE_SAMPLING = "io.sentry.traces.trace-sampling"; static final String TRACE_PROPAGATION_TARGETS = "io.sentry.traces.trace-propagation-targets"; @@ -139,7 +147,7 @@ static void applyMetadata( options.setDebug(readBool(metadata, logger, DEBUG, options.isDebug())); if (options.isDebug()) { - final String level = + final @Nullable String level = readString( metadata, logger, @@ -161,7 +169,7 @@ static void applyMetadata( options.isEnableAutoSessionTracking())); if (options.getSampleRate() == null) { - final Double sampleRate = readDouble(metadata, logger, SAMPLE_RATE); + final double sampleRate = readDouble(metadata, logger, SAMPLE_RATE); if (sampleRate != -1) { options.setSampleRate(sampleRate); } @@ -180,7 +188,7 @@ static void applyMetadata( options.setAttachAnrThreadDump( readBool(metadata, logger, ANR_ATTACH_THREAD_DUMPS, options.isAttachAnrThreadDump())); - final String dsn = readString(metadata, logger, DSN, options.getDsn()); + final @Nullable String dsn = readString(metadata, logger, DSN, options.getDsn()); final boolean enabled = readBool(metadata, logger, ENABLE_SENTRY, options.isEnabled()); if (!enabled || (dsn != null && dsn.isEmpty())) { @@ -293,7 +301,7 @@ static void applyMetadata( options.isCollectAdditionalContext())); if (options.getTracesSampleRate() == null) { - final Double tracesSampleRate = readDouble(metadata, logger, TRACES_SAMPLE_RATE); + final double tracesSampleRate = readDouble(metadata, logger, TRACES_SAMPLE_RATE); if (tracesSampleRate != -1) { options.setTracesSampleRate(tracesSampleRate); } @@ -317,12 +325,42 @@ static void applyMetadata( options.isEnableActivityLifecycleTracingAutoFinish())); if (options.getProfilesSampleRate() == null) { - final Double profilesSampleRate = readDouble(metadata, logger, PROFILES_SAMPLE_RATE); + final double profilesSampleRate = readDouble(metadata, logger, PROFILES_SAMPLE_RATE); if (profilesSampleRate != -1) { options.setProfilesSampleRate(profilesSampleRate); } } + if (options.getProfileSessionSampleRate() == null) { + final double profileSessionSampleRate = + readDouble(metadata, logger, PROFILE_SESSION_SAMPLE_RATE); + if (profileSessionSampleRate != -1) { + options.getExperimental().setProfileSessionSampleRate(profileSessionSampleRate); + } + } + + final @Nullable String profileLifecycle = + readString( + metadata, + logger, + PROFILE_LIFECYCLE, + options.getProfileLifecycle().name().toLowerCase(Locale.ROOT)); + if (profileLifecycle != null) { + options + .getExperimental() + .setProfileLifecycle( + ProfileLifecycle.valueOf(profileLifecycle.toUpperCase(Locale.ROOT))); + } + + options + .getExperimental() + .setStartProfilerOnAppStart( + readBool( + metadata, + logger, + PROFILER_START_ON_APP_START, + options.isStartProfilerOnAppStart())); + options.setEnableUserInteractionTracing( readBool(metadata, logger, TRACES_UI_ENABLE, options.isEnableUserInteractionTracing())); @@ -363,6 +401,7 @@ static void applyMetadata( // sdkInfo.addIntegration(); + @Nullable List integrationsFromGradlePlugin = readList(metadata, logger, SENTRY_GRADLE_PLUGIN_INTEGRATIONS); if (integrationsFromGradlePlugin != null) { @@ -395,7 +434,7 @@ static void applyMetadata( options.isEnableAutoTraceIdGeneration())); if (options.getSessionReplay().getSessionSampleRate() == null) { - final Double sessionSampleRate = + final double sessionSampleRate = readDouble(metadata, logger, REPLAYS_SESSION_SAMPLE_RATE); if (sessionSampleRate != -1) { options.getSessionReplay().setSessionSampleRate(sessionSampleRate); @@ -403,7 +442,7 @@ static void applyMetadata( } if (options.getSessionReplay().getOnErrorSampleRate() == null) { - final Double onErrorSampleRate = readDouble(metadata, logger, REPLAYS_ERROR_SAMPLE_RATE); + final double onErrorSampleRate = readDouble(metadata, logger, REPLAYS_ERROR_SAMPLE_RATE); if (onErrorSampleRate != -1) { options.getSessionReplay().setOnErrorSampleRate(onErrorSampleRate); } @@ -502,7 +541,7 @@ private static boolean readBool( } } - private static @NotNull Double readDouble( + private static double readDouble( final @NotNull Bundle metadata, final @NotNull ILogger logger, final @NotNull String key) { // manifest meta-data only reads float double value = ((Float) metadata.getFloat(key, -1)).doubleValue(); 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 cdb5a13c278..a6d12f4f5b5 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 @@ -9,6 +9,7 @@ import android.net.Uri; import android.os.Process; import android.os.SystemClock; +import io.sentry.IContinuousProfiler; import io.sentry.ILogger; import io.sentry.ISentryLifecycleToken; import io.sentry.ITransactionProfiler; @@ -17,6 +18,7 @@ import io.sentry.SentryExecutorService; import io.sentry.SentryLevel; import io.sentry.SentryOptions; +import io.sentry.TracesSampler; import io.sentry.TracesSamplingDecision; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.android.core.performance.AppStartMetrics; @@ -90,6 +92,11 @@ public void shutdown() { if (appStartProfiler != null) { appStartProfiler.close(); } + final @Nullable IContinuousProfiler appStartContinuousProfiler = + AppStartMetrics.getInstance().getAppStartContinuousProfiler(); + if (appStartContinuousProfiler != null) { + appStartContinuousProfiler.close(); + } } } @@ -122,41 +129,21 @@ private void launchAppStartProfiler(final @NotNull AppStartMetrics appStartMetri return; } + if (profilingOptions.isContinuousProfilingEnabled() + && profilingOptions.isStartProfilerOnAppStart()) { + 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; + if (profilingOptions.isEnableAppStartProfiling()) { + createAndStartTransactionProfiler(context, profilingOptions, appStartMetrics); } - 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(); - } catch (FileNotFoundException e) { logger.log(SentryLevel.ERROR, "App start profiling config file not found. ", e); } catch (Throwable e) { @@ -164,6 +151,71 @@ private void launchAppStartProfiler(final @NotNull AppStartMetrics appStartMetri } } + private void createAndStartContinuousProfiler( + final @NotNull Context context, + final @NotNull SentryAppStartProfilingOptions profilingOptions, + final @NotNull AppStartMetrics appStartMetrics) { + + if (!profilingOptions.isContinuousProfileSampled()) { + logger.log(SentryLevel.DEBUG, "App start profiling was not sampled. It will not start."); + return; + } + + 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."); + SentryOptions sentryOptions = SentryOptions.empty(); + // Let's fake a sampler to accept the sampling decision that was calculated on last run + sentryOptions + .getExperimental() + .setProfileSessionSampleRate(profilingOptions.isContinuousProfileSampled() ? 1.0 : 0.0); + appStartContinuousProfiler.startProfileSession( + profilingOptions.getProfileLifecycle(), new TracesSampler(sentryOptions)); + } + + 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 15781d711fb..ccd4a92b276 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 a2ca74b3607..0cc7cdca8e2 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.NoOpLogger; @@ -63,6 +64,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 boolean isCallbackRegistered = false; private boolean shouldSendStartMeasurements = true; @@ -222,6 +224,10 @@ public void clear() { appStartProfiler.close(); } appStartProfiler = null; + if (appStartContinuousProfiler != null) { + appStartContinuousProfiler.close(); + } + appStartContinuousProfiler = null; appStartSamplingDecision = null; appLaunchedInForeground = false; isCallbackRegistered = false; @@ -238,6 +244,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; @@ -312,11 +327,15 @@ private void checkCreateTimeOnMain() { if (activeActivitiesCounter.get() == 0) { 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; + } } }); } 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 new file mode 100644 index 00000000000..542792b8a98 --- /dev/null +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidContinuousProfilerTest.kt @@ -0,0 +1,552 @@ +package io.sentry.android.core + +import android.content.Context +import android.os.Build +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.sentry.CompositePerformanceCollector +import io.sentry.CpuCollectionData +import io.sentry.DataCategory +import io.sentry.IConnectionStatusProvider +import io.sentry.ILogger +import io.sentry.IScopes +import io.sentry.ISentryExecutorService +import io.sentry.MemoryCollectionData +import io.sentry.PerformanceCollectionData +import io.sentry.ProfileLifecycle +import io.sentry.Sentry +import io.sentry.SentryLevel +import io.sentry.SentryNanotimeDate +import io.sentry.SentryTracer +import io.sentry.TracesSampler +import io.sentry.TransactionContext +import io.sentry.android.core.internal.util.SentryFrameMetricsCollector +import io.sentry.profilemeasurements.ProfileMeasurement +import io.sentry.protocol.SentryId +import io.sentry.test.DeferredExecutorService +import io.sentry.test.getProperty +import io.sentry.transport.RateLimiter +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.Mockito.mockStatic +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 +import org.mockito.kotlin.spy +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import java.io.File +import java.util.concurrent.Callable +import java.util.concurrent.Future +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertContains +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +class AndroidContinuousProfilerTest { + private lateinit var context: Context + private val fixture = Fixture() + + private class Fixture { + private val mockDsn = "http://key@localhost/proj" + val buildInfo = mock { + whenever(it.sdkInfoVersion).thenReturn(Build.VERSION_CODES.LOLLIPOP_MR1) + } + val mockedSentry = mockStatic(Sentry::class.java) + val mockLogger = mock() + val mockTracesSampler = mock() + + val scopes: IScopes = mock() + val frameMetricsCollector: SentryFrameMetricsCollector = mock() + + lateinit var transaction1: SentryTracer + lateinit var transaction2: SentryTracer + lateinit var transaction3: SentryTracer + + val options = spy(SentryAndroidOptions()).apply { + dsn = mockDsn + profilesSampleRate = 1.0 + isDebug = true + setLogger(mockLogger) + } + + init { + whenever(mockTracesSampler.sampleSessionProfile(any())).thenReturn(true) + } + + fun getSut(buildInfoProvider: BuildInfoProvider = buildInfo, optionConfig: ((options: SentryAndroidOptions) -> Unit) = {}): AndroidContinuousProfiler { + optionConfig(options) + 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.profilingTracesHz, + options.executorService + ) + } + } + + @BeforeTest + fun `set up`() { + context = ApplicationProvider.getApplicationContext() + val buildInfoProvider = BuildInfoProvider(fixture.mockLogger) + val loadClass = LoadClass() + val activityFramesTracker = ActivityFramesTracker(loadClass, fixture.options) + AndroidOptionsInitializer.loadDefaultAndMetadataOptions( + fixture.options, + context, + fixture.mockLogger, + buildInfoProvider + ) + + AndroidOptionsInitializer.installDefaultIntegrations( + context, + fixture.options, + buildInfoProvider, + loadClass, + activityFramesTracker, + false, + false, + false + ) + + AndroidOptionsInitializer.initializeIntegrationsAndProcessors( + fixture.options, + context, + buildInfoProvider, + loadClass, + activityFramesTracker + ) + // 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) + + fixture.mockedSentry.`when` { Sentry.getCurrentScopes() }.thenReturn(fixture.scopes) + } + + @AfterTest + fun clear() { + context.cacheDir.deleteRecursively() + fixture.mockedSentry.close() + } + + @Test + fun `isRunning reflects profiler status`() { + val profiler = fixture.getSut() + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertTrue(profiler.isRunning) + profiler.stopProfileSession(ProfileLifecycle.MANUAL) + assertFalse(profiler.isRunning) + } + + @Test + fun `profiler multiple starts are ignored in manual mode`() { + val profiler = fixture.getSut() + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertTrue(profiler.isRunning) + verify(fixture.mockLogger, never()).log(eq(SentryLevel.DEBUG), eq("Profiler is already running.")) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + verify(fixture.mockLogger).log(eq(SentryLevel.DEBUG), eq("Profiler is already running.")) + assertTrue(profiler.isRunning) + assertEquals(0, profiler.rootSpanCounter) + } + + @Test + fun `profiler multiple starts are accepted in trace mode`() { + val profiler = fixture.getSut() + + // rootSpanCounter is incremented when the profiler starts in trace mode + assertEquals(0, profiler.rootSpanCounter) + profiler.startProfileSession(ProfileLifecycle.TRACE, fixture.mockTracesSampler) + assertEquals(1, profiler.rootSpanCounter) + assertTrue(profiler.isRunning) + profiler.startProfileSession(ProfileLifecycle.TRACE, fixture.mockTracesSampler) + verify(fixture.mockLogger, never()).log(eq(SentryLevel.DEBUG), eq("Profiler is already running.")) + assertTrue(profiler.isRunning) + assertEquals(2, profiler.rootSpanCounter) + + // rootSpanCounter is decremented when the profiler stops in trace mode, and keeps running until rootSpanCounter is 0 + profiler.stopProfileSession(ProfileLifecycle.TRACE) + assertEquals(1, profiler.rootSpanCounter) + assertTrue(profiler.isRunning) + + // only when rootSpanCounter is 0 the profiler stops + profiler.stopProfileSession(ProfileLifecycle.TRACE) + assertEquals(0, profiler.rootSpanCounter) + assertFalse(profiler.isRunning) + } + + @Test + fun `profiler logs a warning on start if not sampled`() { + val profiler = fixture.getSut() + whenever(fixture.mockTracesSampler.sampleSessionProfile(any())).thenReturn(false) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertFalse(profiler.isRunning) + verify(fixture.mockLogger).log(eq(SentryLevel.DEBUG), eq("Profiler was not started due to sampling decision.")) + } + + @Test + fun `profiler evaluates sessionSampleRate only the first time`() { + val profiler = fixture.getSut() + verify(fixture.mockTracesSampler, never()).sampleSessionProfile(any()) + // The first time the profiler is started, the sessionSampleRate is evaluated + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + verify(fixture.mockTracesSampler, times(1)).sampleSessionProfile(any()) + // Then, the sessionSampleRate is not evaluated again + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + verify(fixture.mockTracesSampler, times(1)).sampleSessionProfile(any()) + } + + @Test + fun `when reevaluateSampling, profiler evaluates sessionSampleRate on next start`() { + val profiler = fixture.getSut() + verify(fixture.mockTracesSampler, never()).sampleSessionProfile(any()) + // The first time the profiler is started, the sessionSampleRate is evaluated + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + verify(fixture.mockTracesSampler, times(1)).sampleSessionProfile(any()) + // When reevaluateSampling is called, the sessionSampleRate is not evaluated immediately + profiler.reevaluateSampling() + verify(fixture.mockTracesSampler, times(1)).sampleSessionProfile(any()) + // Then, when the profiler starts again, the sessionSampleRate is reevaluated + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + verify(fixture.mockTracesSampler, times(2)).sampleSessionProfile(any()) + } + + @Test + fun `profiler works only on api 22+`() { + val buildInfo = mock { + whenever(it.sdkInfoVersion).thenReturn(Build.VERSION_CODES.LOLLIPOP) + } + val profiler = fixture.getSut(buildInfo) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertFalse(profiler.isRunning) + } + + @Test + fun `profiler ignores profilesSampleRate`() { + val profiler = fixture.getSut { + it.profilesSampleRate = 0.0 + } + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertTrue(profiler.isRunning) + } + + @Test + fun `profiler evaluates profilingTracesDirPath options only on first start`() { + // We create the profiler, and nothing goes wrong + val profiler = fixture.getSut { + it.cacheDirPath = null + } + verify(fixture.mockLogger, never()).log( + SentryLevel.WARNING, + "Disabling profiling because no profiling traces dir path is defined in options." + ) + + // Regardless of how many times the profiler is started, the option is evaluated and logged only once + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + verify(fixture.mockLogger, times(1)).log( + SentryLevel.WARNING, + "Disabling profiling because no profiling traces dir path is defined in options." + ) + } + + @Test + fun `profiler evaluates profilingTracesHz options only on first start`() { + // We create the profiler, and nothing goes wrong + val profiler = fixture.getSut { + it.profilingTracesHz = 0 + } + verify(fixture.mockLogger, never()).log( + SentryLevel.WARNING, + "Disabling profiling because trace rate is set to %d", + 0 + ) + + // Regardless of how many times the profiler is started, the option is evaluated and logged only once + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + verify(fixture.mockLogger, times(1)).log( + SentryLevel.WARNING, + "Disabling profiling because trace rate is set to %d", + 0 + ) + } + + @Test + fun `profiler on tracesDirPath null`() { + val profiler = fixture.getSut { + it.cacheDirPath = null + } + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertFalse(profiler.isRunning) + } + + @Test + fun `profiler on tracesDirPath empty`() { + val profiler = fixture.getSut { + it.cacheDirPath = "" + } + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertFalse(profiler.isRunning) + } + + @Test + fun `profiler on profilingTracesHz 0`() { + val profiler = fixture.getSut { + it.profilingTracesHz = 0 + } + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertFalse(profiler.isRunning) + } + + @Test + fun `profiler never use background threads`() { + val mockExecutorService: ISentryExecutorService = mock() + val profiler = fixture.getSut { + it.executorService = mockExecutorService + } + whenever(mockExecutorService.submit(any>())).thenReturn(mock()) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + verify(mockExecutorService, never()).submit(any()) + profiler.stopProfileSession(ProfileLifecycle.MANUAL) + verify(mockExecutorService, never()).submit(any>()) + } + + @Test + fun `profiler does not throw if traces cannot be written to disk`() { + val profiler = fixture.getSut { + File(it.profilingTracesDirPath!!).setWritable(false) + } + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + profiler.stopProfileSession(ProfileLifecycle.MANUAL) + // We assert that no trace files are written + assertTrue( + File(fixture.options.profilingTracesDirPath!!) + .list()!! + .isEmpty() + ) + 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.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + verify(performanceCollector).start(any()) + } + + @Test + fun `profiler stops performance collector on stop`() { + val performanceCollector = mock() + fixture.options.compositePerformanceCollector = performanceCollector + val profiler = fixture.getSut() + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + verify(performanceCollector, never()).stop(any()) + profiler.stopProfileSession(ProfileLifecycle.MANUAL) + verify(performanceCollector).stop(any()) + } + + @Test + fun `profiler stops collecting frame metrics when it stops`() { + val profiler = fixture.getSut() + val frameMetricsCollectorId = "id" + whenever(fixture.frameMetricsCollector.startCollection(any())).thenReturn(frameMetricsCollectorId) + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + verify(fixture.frameMetricsCollector, never()).stopCollection(frameMetricsCollectorId) + profiler.stopProfileSession(ProfileLifecycle.MANUAL) + verify(fixture.frameMetricsCollector).stopCollection(frameMetricsCollectorId) + } + + @Test + fun `profiler stops profiling and clear scheduled job on close`() { + val profiler = fixture.getSut() + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertTrue(profiler.isRunning) + + profiler.close() + assertFalse(profiler.isRunning) + + // The timeout scheduled job should be cleared + val androidProfiler = profiler.getProperty("profiler") + val scheduledJob = androidProfiler?.getProperty?>("scheduledFinish") + assertNull(scheduledJob) + + val stopFuture = profiler.stopFuture + assertNotNull(stopFuture) + assertTrue(stopFuture.isCancelled) + } + + @Test + fun `profiler stops and restart for each chunk`() { + val executorService = DeferredExecutorService() + val profiler = fixture.getSut { + it.executorService = executorService + } + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertTrue(profiler.isRunning) + + executorService.runAll() + verify(fixture.mockLogger).log(eq(SentryLevel.DEBUG), eq("Profile chunk finished. Starting a new one.")) + assertTrue(profiler.isRunning) + + executorService.runAll() + 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.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + 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.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + profiler.stopProfileSession(ProfileLifecycle.MANUAL) + // 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.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + 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.stopProfileSession(ProfileLifecycle.MANUAL) + // Now the executor is used to send the chunk + executorService.runAll() + verify(fixture.scopes, times(2)).captureProfileChunk(any()) + } + + @Test + fun `profiler does not send chunks after close`() { + val executorService = DeferredExecutorService() + val profiler = fixture.getSut { + it.executorService = executorService + } + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertTrue(profiler.isRunning) + + // We close the profiler, which should prevent sending additional chunks + profiler.close() + + // The executor used to send the chunk doesn't do anything + executorService.runAll() + verify(fixture.scopes, never()).captureProfileChunk(any()) + } + + @Test + fun `profiler stops when rate limited`() { + val executorService = DeferredExecutorService() + val profiler = fixture.getSut { + it.executorService = executorService + } + val rateLimiter = mock() + whenever(rateLimiter.isActiveForCategory(DataCategory.ProfileChunk)).thenReturn(true) + + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertTrue(profiler.isRunning) + + // If the SDK is rate limited, the profiler should stop + profiler.onRateLimitChanged(rateLimiter) + assertFalse(profiler.isRunning) + assertEquals(SentryId.EMPTY_ID, profiler.profilerId) + verify(fixture.mockLogger).log(eq(SentryLevel.WARNING), eq("SDK is rate limited. Stopping profiler.")) + } + + @Test + fun `profiler does not start when rate limited`() { + val executorService = DeferredExecutorService() + val profiler = fixture.getSut { + it.executorService = executorService + } + val rateLimiter = mock() + whenever(rateLimiter.isActiveForCategory(DataCategory.ProfileChunk)).thenReturn(true) + whenever(fixture.scopes.rateLimiter).thenReturn(rateLimiter) + + // If the SDK is rate limited, the profiler should never start + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertFalse(profiler.isRunning) + assertEquals(SentryId.EMPTY_ID, profiler.profilerId) + verify(fixture.mockLogger).log(eq(SentryLevel.WARNING), eq("SDK is rate limited. Stopping profiler.")) + } + + @Test + fun `profiler does not start when offline`() { + val executorService = DeferredExecutorService() + val profiler = fixture.getSut { + it.executorService = executorService + it.connectionStatusProvider = mock { provider -> + whenever(provider.connectionStatus).thenReturn(IConnectionStatusProvider.ConnectionStatus.DISCONNECTED) + } + } + + // If the device is offline, the profiler should never start + profiler.startProfileSession(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertFalse(profiler.isRunning) + assertEquals(SentryId.EMPTY_ID, profiler.profilerId) + verify(fixture.mockLogger).log(eq(SentryLevel.WARNING), eq("Device is offline. Stopping profiler.")) + } + + fun withMockScopes(closure: () -> Unit) = Mockito.mockStatic(Sentry::class.java).use { + it.`when` { Sentry.getCurrentScopes() }.thenReturn(fixture.scopes) + closure.invoke() + } +} 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 7f54b2c3539..f86415037a5 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 7879c2daf53..be41874a83a 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 401e65fa1ab..769510479f8 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 @@ -36,6 +41,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 @@ -347,11 +353,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 @@ -666,10 +774,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 670729c4d76..86e0d90001f 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 b92db7cfc62..ec9e737559b 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/ManifestMetadataReaderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt index 263c9c5950c..fcfb4bf814b 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt @@ -6,6 +6,7 @@ import androidx.core.os.bundleOf import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.FilterString import io.sentry.ILogger +import io.sentry.ProfileLifecycle import io.sentry.SentryLevel import io.sentry.SentryReplayOptions import org.junit.runner.RunWith @@ -807,6 +808,98 @@ class ManifestMetadataReaderTest { assertNull(fixture.options.profilesSampleRate) } + @Test + fun `applyMetadata reads profileSessionSampleRate from metadata`() { + // Arrange + val expectedSampleRate = 0.99f + val bundle = bundleOf(ManifestMetadataReader.PROFILE_SESSION_SAMPLE_RATE to expectedSampleRate) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertEquals(expectedSampleRate.toDouble(), fixture.options.profileSessionSampleRate) + } + + @Test + fun `applyMetadata does not override profileSessionSampleRate from options`() { + // Arrange + val expectedSampleRate = 0.99f + fixture.options.experimental.profileSessionSampleRate = expectedSampleRate.toDouble() + val bundle = bundleOf(ManifestMetadataReader.PROFILE_SESSION_SAMPLE_RATE to 0.1f) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertEquals(expectedSampleRate.toDouble(), fixture.options.profileSessionSampleRate) + } + + @Test + fun `applyMetadata without specifying profileSessionSampleRate, stays null`() { + // Arrange + val context = fixture.getContext() + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertNull(fixture.options.profileSessionSampleRate) + } + + @Test + fun `applyMetadata without specifying profileLifecycle, stays MANUAL`() { + // Arrange + val context = fixture.getContext() + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertEquals(ProfileLifecycle.MANUAL, fixture.options.profileLifecycle) + } + + @Test + fun `applyMetadata reads profileLifecycle from metadata`() { + // Arrange + val expectedLifecycle = "trace" + val bundle = bundleOf(ManifestMetadataReader.PROFILE_LIFECYCLE to expectedLifecycle) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertEquals(ProfileLifecycle.TRACE, fixture.options.profileLifecycle) + } + + @Test + fun `applyMetadata without specifying isStartProfilerOnAppStart, stays false`() { + // Arrange + val context = fixture.getContext() + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertFalse(fixture.options.isStartProfilerOnAppStart) + } + + @Test + fun `applyMetadata reads isStartProfilerOnAppStart from metadata`() { + // Arrange + val bundle = bundleOf(ManifestMetadataReader.PROFILER_START_ON_APP_START to true) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options, fixture.buildInfoProvider) + + // Assert + assertTrue(fixture.options.isStartProfilerOnAppStart) + } + @Test fun `applyMetadata reads tracePropagationTargets to options`() { // Arrange 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 4317751c59a..728f78da241 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 @@ -124,6 +124,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()) } @@ -134,6 +135,7 @@ class SentryPerformanceProviderTest { config.setReadable(false) } assertNull(AppStartMetrics.getInstance().appStartProfiler) + assertNull(AppStartMetrics.getInstance().appStartContinuousProfiler) verify(fixture.logger, never()).log(any(), any()) } @@ -143,6 +145,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.") @@ -152,7 +155,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( @@ -161,10 +164,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) @@ -180,7 +195,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) @@ -193,10 +208,38 @@ class SentryPerformanceProviderTest { } @Test - fun `when profiler starts, it is set in AppStartMetrics`() { + fun `when continuous profile is not sampled, continuous profiler is not started`() { + fixture.getSut { config -> + writeConfig(config, continuousProfileSampled = false) + } + assertNull(AppStartMetrics.getInstance().appStartProfiler) + assertNull(AppStartMetrics.getInstance().appStartContinuousProfiler) + verify(fixture.logger).log( + eq(SentryLevel.DEBUG), + eq("App start profiling was not sampled. It will not start.") + ) + } + + // This case should never happen in reality, but it's technically possible to have such configuration + @Test + 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) @@ -208,33 +251,80 @@ 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 isEnableAppStartProfiling is false, transaction profiler is not started`() { + fixture.getSut { config -> + writeConfig(config, profilingEnabled = true, continuousProfilingEnabled = false, isEnableAppStartProfiling = false) + } + assertNull(AppStartMetrics.getInstance().appStartProfiler) + } + + @Test + fun `when isStartProfilerOnAppStart is false, continuous profiler is not started`() { + fixture.getSut { config -> + writeConfig(config, profilingEnabled = false, continuousProfilingEnabled = true, isStartProfilerOnAppStart = false) + } + assertNull(AppStartMetrics.getInstance().appStartContinuousProfiler) + } + + @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, profileSampleRate: Double = 1.0, + continuousProfileSampled: Boolean = true, + isEnableAppStartProfiling: Boolean = true, + isStartProfilerOnAppStart: Boolean = true, profilingTracesDirPath: String = traceDir.absolutePath ) { val appStartProfilingOptions = SentryAppStartProfilingOptions() appStartProfilingOptions.isProfilingEnabled = profilingEnabled + appStartProfilingOptions.isContinuousProfilingEnabled = continuousProfilingEnabled appStartProfilingOptions.isTraceSampled = traceSampled appStartProfilingOptions.traceSampleRate = traceSampleRate appStartProfilingOptions.isProfileSampled = profileSampled appStartProfilingOptions.profileSampleRate = profileSampleRate + appStartProfilingOptions.isContinuousProfileSampled = continuousProfileSampled appStartProfilingOptions.profilingTracesDirPath = profilingTracesDirPath appStartProfilingOptions.profilingTracesHz = 101 + appStartProfilingOptions.isEnableAppStartProfiling = isEnableAppStartProfiling + appStartProfilingOptions.isStartProfilerOnAppStart = isStartProfilerOnAppStart JsonSerializer(SentryOptions.empty()).serialize(appStartProfilingOptions, FileWriter(configFile)) } //endregion 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 f6fb464bf3b..f22d4ed7cea 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 eb59f0732e1..0b3729f8ce4 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 81bd8b7945e..f8734a26f00 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 @@ -9,6 +9,7 @@ import android.os.Looper import android.os.SystemClock import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.DateUtils +import io.sentry.IContinuousProfiler import io.sentry.ITransactionProfiler import io.sentry.SentryNanotimeDate import io.sentry.android.core.SentryAndroidOptions @@ -63,6 +64,7 @@ class AppStartMetricsTest { AppStartMetrics.onApplicationCreate(mock()) AppStartMetrics.onContentProviderCreate(mock()) metrics.appStartProfiler = mock() + metrics.appStartContinuousProfiler = mock() metrics.appStartSamplingDecision = mock() metrics.clear() @@ -75,6 +77,7 @@ class AppStartMetricsTest { assertTrue(metrics.activityLifecycleTimeSpans.isEmpty()) assertTrue(metrics.contentProviderOnCreateTimeSpans.isEmpty()) assertNull(metrics.appStartProfiler) + assertNull(metrics.appStartContinuousProfiler) assertNull(metrics.appStartSamplingDecision) } @@ -260,6 +263,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().registerLifecycleCallbacks(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() @@ -274,6 +290,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().registerLifecycleCallbacks(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 17c37d69bfc..b292f0d038f 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-bootstrap/api/sentry-opentelemetry-bootstrap.api b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api index 8eeb9936f7d..8e6b59b4be2 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/api/sentry-opentelemetry-bootstrap.api @@ -38,7 +38,7 @@ public final class io/sentry/opentelemetry/OtelSpanFactory : io/sentry/ISpanFact public fun ()V public fun (Lio/opentelemetry/api/OpenTelemetry;)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/OtelStrongRefSpanWrapper : io/sentry/opentelemetry/IOtelSpanWrapper { diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelSpanFactory.java b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelSpanFactory.java index 7a51c3f337d..f64d07e8c3c 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelSpanFactory.java +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/src/main/java/io/sentry/opentelemetry/OtelSpanFactory.java @@ -11,6 +11,7 @@ import io.opentelemetry.context.Context; import io.sentry.Baggage; import io.sentry.BuildConfig; +import io.sentry.CompositePerformanceCollector; import io.sentry.IScopes; import io.sentry.ISpan; import io.sentry.ISpanFactory; @@ -24,7 +25,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; @@ -51,7 +51,7 @@ public OtelSpanFactory() { @NotNull TransactionContext context, @NotNull IScopes scopes, @NotNull TransactionOptions transactionOptions, - @Nullable TransactionPerformanceCollector transactionPerformanceCollector) { + @Nullable CompositePerformanceCollector compositePerformanceCollector) { final @Nullable IOtelSpanWrapper 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 7fa16daae52..dbda0b0a1a2 100644 --- a/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml +++ b/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml @@ -110,10 +110,14 @@ - - - - + + + + + + + + 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 a4a1c5397a9..13117e39e6c 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.startProfileSession(); 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 c7b732f4602..b5bd831c88c 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(3)).options + verify(fixture.scopes, times(4)).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 6eba98d1d2e..5a30bc86c97 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(3)).options + verify(fixture.scopes, times(4)).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 96fc98eebd7..307e1785054 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -324,10 +324,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 { @@ -344,6 +354,7 @@ public final class io/sentry/DataCategory : java/lang/Enum { public static final field Error Lio/sentry/DataCategory; public static final field Monitor Lio/sentry/DataCategory; public static final field Profile Lio/sentry/DataCategory; + public static final field ProfileChunk Lio/sentry/DataCategory; public static final field Replay Lio/sentry/DataCategory; public static final field Security Lio/sentry/DataCategory; public static final field Session Lio/sentry/DataCategory; @@ -380,6 +391,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 @@ -391,16 +413,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 { @@ -443,6 +456,12 @@ public abstract interface class io/sentry/EventProcessor { public final class io/sentry/ExperimentalOptions { public fun (ZLio/sentry/protocol/SdkVersion;)V + public fun getProfileLifecycle ()Lio/sentry/ProfileLifecycle; + public fun getProfileSessionSampleRate ()Ljava/lang/Double; + public fun isStartProfilerOnAppStart ()Z + public fun setProfileLifecycle (Lio/sentry/ProfileLifecycle;)V + public fun setProfileSessionSampleRate (Ljava/lang/Double;)V + public fun setStartProfilerOnAppStart (Z)V } public final class io/sentry/ExternalOptions { @@ -586,6 +605,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 @@ -632,8 +652,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 startProfileSession ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfileSession ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -651,6 +673,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 @@ -696,8 +719,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 startProfileSession ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfileSession ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -722,6 +747,15 @@ public abstract interface class io/sentry/IConnectionStatusProvider$IConnectionS public abstract fun onConnectionStatusChanged (Lio/sentry/IConnectionStatusProvider$ConnectionStatus;)V } +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 reevaluateSampling ()V + public abstract fun startProfileSession (Lio/sentry/ProfileLifecycle;Lio/sentry/TracesSampler;)V + public abstract fun stopProfileSession (Lio/sentry/ProfileLifecycle;)V +} + public abstract interface class io/sentry/IEnvelopeReader { public abstract fun read (Ljava/io/InputStream;)Lio/sentry/SentryEnvelope; } @@ -874,6 +908,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; @@ -923,11 +958,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 startProfileSession ()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 stopProfileSession ()V public abstract fun withIsolationScope (Lio/sentry/ScopeCallback;)V public abstract fun withScope (Lio/sentry/ScopeCallback;)V } @@ -953,6 +990,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 @@ -1033,7 +1071,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 { @@ -1267,9 +1305,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 } @@ -1373,6 +1411,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 @@ -1381,6 +1430,16 @@ public final class io/sentry/NoOpConnectionStatusProvider : io/sentry/IConnectio public fun removeConnectionStatusObserver (Lio/sentry/IConnectionStatusProvider$IConnectionStatusObserver;)V } +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 reevaluateSampling ()V + public fun startProfileSession (Lio/sentry/ProfileLifecycle;Lio/sentry/TracesSampler;)V + public fun stopProfileSession (Lio/sentry/ProfileLifecycle;)V +} + public final class io/sentry/NoOpEnvelopeReader : io/sentry/IEnvelopeReader { public static fun getInstance ()Lio/sentry/NoOpEnvelopeReader; public fun read (Ljava/io/InputStream;)Lio/sentry/SentryEnvelope; @@ -1398,6 +1457,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 @@ -1445,8 +1505,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 startProfileSession ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfileSession ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -1557,6 +1619,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 @@ -1604,8 +1667,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 startProfileSession ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfileSession ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -1666,7 +1731,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; } @@ -1723,15 +1788,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 @@ -1817,6 +1873,87 @@ 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;Ljava/lang/Double;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 getTimestamp ()D + 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;Lio/sentry/SentryDate;)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 TIMESTAMP 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/ProfileLifecycle : java/lang/Enum { + public static final field MANUAL Lio/sentry/ProfileLifecycle; + public static final field TRACE Lio/sentry/ProfileLifecycle; + public static fun valueOf (Ljava/lang/String;)Lio/sentry/ProfileLifecycle; + public static fun values ()[Lio/sentry/ProfileLifecycle; +} + 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; @@ -2160,6 +2297,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 @@ -2206,8 +2344,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 startProfileSession ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfileSession ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -2224,6 +2364,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 @@ -2270,8 +2411,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 startProfileSession ()V public fun startSession ()V public fun startTransaction (Lio/sentry/TransactionContext;Lio/sentry/TransactionOptions;)Lio/sentry/ITransaction; + public fun stopProfileSession ()V public fun withIsolationScope (Lio/sentry/ScopeCallback;)V public fun withScope (Lio/sentry/ScopeCallback;)V } @@ -2374,12 +2517,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 startProfileSession ()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 stopProfileSession ()V public static fun withIsolationScope (Lio/sentry/ScopeCallback;)V public static fun withScope (Lio/sentry/ScopeCallback;)V } @@ -2390,20 +2535,30 @@ public abstract interface class io/sentry/Sentry$OptionsConfiguration { public final class io/sentry/SentryAppStartProfilingOptions : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun ()V + public fun getProfileLifecycle ()Lio/sentry/ProfileLifecycle; public fun getProfileSampleRate ()Ljava/lang/Double; public fun getProfilingTracesDirPath ()Ljava/lang/String; public fun getProfilingTracesHz ()I public fun getTraceSampleRate ()Ljava/lang/Double; public fun getUnknown ()Ljava/util/Map; + public fun isContinuousProfileSampled ()Z + public fun isContinuousProfilingEnabled ()Z + public fun isEnableAppStartProfiling ()Z public fun isProfileSampled ()Z public fun isProfilingEnabled ()Z + public fun isStartProfilerOnAppStart ()Z public fun isTraceSampled ()Z public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public fun setContinuousProfileSampled (Z)V + public fun setContinuousProfilingEnabled (Z)V + public fun setEnableAppStartProfiling (Z)V + public fun setProfileLifecycle (Lio/sentry/ProfileLifecycle;)V public fun setProfileSampleRate (Ljava/lang/Double;)V public fun setProfileSampled (Z)V public fun setProfilingEnabled (Z)V public fun setProfilingTracesDirPath (Ljava/lang/String;)V public fun setProfilingTracesHz (I)V + public fun setStartProfilerOnAppStart (Z)V public fun setTraceSampleRate (Ljava/lang/Double;)V public fun setTraceSampled (Z)V public fun setUnknown (Ljava/util/Map;)V @@ -2416,7 +2571,12 @@ public final class io/sentry/SentryAppStartProfilingOptions$Deserializer : io/se } public final class io/sentry/SentryAppStartProfilingOptions$JsonKeys { + public static final field CONTINUOUS_PROFILE_SAMPLED Ljava/lang/String; + public static final field IS_CONTINUOUS_PROFILING_ENABLED Ljava/lang/String; + public static final field IS_ENABLE_APP_START_PROFILING Ljava/lang/String; public static final field IS_PROFILING_ENABLED Ljava/lang/String; + public static final field IS_START_PROFILER_ON_APP_START Ljava/lang/String; + public static final field PROFILE_LIFECYCLE Ljava/lang/String; public static final field PROFILE_SAMPLED Ljava/lang/String; public static final field PROFILE_SAMPLE_RATE Ljava/lang/String; public static final field PROFILING_TRACES_DIR_PATH Ljava/lang/String; @@ -2509,6 +2669,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; @@ -2588,6 +2749,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; @@ -2719,6 +2881,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; @@ -2856,9 +3019,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; @@ -2902,6 +3067,8 @@ public class io/sentry/SentryOptions { public fun getOptionsObservers ()Ljava/util/List; public fun getOutboxPath ()Ljava/lang/String; public fun getPerformanceCollectors ()Ljava/util/List; + public fun getProfileLifecycle ()Lio/sentry/ProfileLifecycle; + public fun getProfileSessionSampleRate ()Ljava/lang/Double; public fun getProfilesSampleRate ()Ljava/lang/Double; public fun getProfilesSampler ()Lio/sentry/SentryOptions$ProfilesSamplerCallback; public fun getProfilingTracesDirPath ()Ljava/lang/String; @@ -2929,7 +3096,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; @@ -2938,6 +3104,7 @@ public class io/sentry/SentryOptions { public fun isAttachStacktrace ()Z public fun isAttachThreads ()Z public fun isCaptureOpenTelemetryEvents ()Z + public fun isContinuousProfilingEnabled ()Z public fun isDebug ()Z public fun isEnableAppStartProfiling ()Z public fun isEnableAutoSessionTracking ()Z @@ -2961,6 +3128,7 @@ public class io/sentry/SentryOptions { public fun isSendClientReports ()Z public fun isSendDefaultPii ()Z public fun isSendModules ()Z + public fun isStartProfilerOnAppStart ()Z public fun isTraceOptionsRequests ()Z public fun isTraceSampling ()Z public fun isTracingEnabled ()Z @@ -2976,8 +3144,10 @@ public class io/sentry/SentryOptions { public fun setBeforeSendTransaction (Lio/sentry/SentryOptions$BeforeSendTransactionCallback;)V public fun setCacheDirPath (Ljava/lang/String;)V public fun setCaptureOpenTelemetryEvents (Z)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 @@ -3060,7 +3230,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 @@ -3555,6 +3724,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; } @@ -3690,6 +3860,7 @@ public final class io/sentry/TraceContext$JsonKeys { public final class io/sentry/TracesSampler { public fun (Lio/sentry/SentryOptions;)V public fun sample (Lio/sentry/SamplingContext;)Lio/sentry/TracesSamplingDecision; + public fun sampleSessionProfile (D)Z } public final class io/sentry/TracesSamplingDecision { @@ -3750,14 +3921,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; @@ -4371,9 +4534,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 @@ -4389,6 +4553,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 } @@ -4492,6 +4657,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 @@ -4511,6 +4677,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 setSpring (Lio/sentry/protocol/Spring;)V @@ -4574,6 +4741,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; @@ -6380,6 +6548,7 @@ public final class io/sentry/util/SampleRateUtils { public fun ()V public static fun backfilledSampleRand (Lio/sentry/TracesSamplingDecision;)Lio/sentry/TracesSamplingDecision; public static fun backfilledSampleRand (Ljava/lang/Double;Ljava/lang/Double;Ljava/lang/Boolean;)Ljava/lang/Double; + public static fun isValidContinuousProfilesSampleRate (Ljava/lang/Double;)Z public static fun isValidProfilesSampleRate (Ljava/lang/Double;)Z public static fun isValidSampleRate (Ljava/lang/Double;)Z public static fun isValidTracesSampleRate (Ljava/lang/Double;)Z @@ -6464,6 +6633,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 @@ -6473,6 +6643,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 @@ -6482,6 +6653,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 7880d611975..e6238679a79 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 081063a53f4..bcbab7c136a 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/DataCategory.java b/sentry/src/main/java/io/sentry/DataCategory.java index 43181eaeac0..bbee8f2e849 100644 --- a/sentry/src/main/java/io/sentry/DataCategory.java +++ b/sentry/src/main/java/io/sentry/DataCategory.java @@ -12,6 +12,7 @@ public enum DataCategory { Attachment("attachment"), Monitor("monitor"), Profile("profile"), + ProfileChunk("profile_chunk"), Transaction("transaction"), Replay("replay"), Span("span"), 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 9489842b5c3..ae99fe00c74 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 56aed3b92d1..f664360f49b 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/ExperimentalOptions.java b/sentry/src/main/java/io/sentry/ExperimentalOptions.java index 80d59d4f01b..f78624bfe3a 100644 --- a/sentry/src/main/java/io/sentry/ExperimentalOptions.java +++ b/sentry/src/main/java/io/sentry/ExperimentalOptions.java @@ -1,6 +1,9 @@ package io.sentry; import io.sentry.protocol.SdkVersion; +import io.sentry.util.SampleRateUtils; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** @@ -11,5 +14,74 @@ */ public final class ExperimentalOptions { + /** + * Indicates the percentage in which the profiles for the session will be created. Specifying 0 + * means never, 1.0 means always. The value needs to be >= 0.0 and <= 1.0 The default is null + * (disabled). + */ + private @Nullable Double profileSessionSampleRate; + + /** + * Whether the profiling lifecycle is controlled manually or based on the trace lifecycle. + * Defaults to {@link ProfileLifecycle#MANUAL}. + */ + private @NotNull ProfileLifecycle profileLifecycle = ProfileLifecycle.MANUAL; + + /** + * Whether profiling can automatically be started as early as possible during the app lifecycle, + * to capture more of app startup. If {@link ExperimentalOptions#profileLifecycle} is {@link + * ProfileLifecycle#MANUAL} Profiling is started automatically on startup and stopProfileSession + * must be called manually whenever the app startup is completed If {@link + * ExperimentalOptions#profileLifecycle} is {@link ProfileLifecycle#TRACE} Profiling is started + * automatically on startup, and will automatically be stopped when the root span that is + * associated with app startup ends + */ + private boolean startProfilerOnAppStart = false; + public ExperimentalOptions(final boolean empty, final @Nullable SdkVersion sdkVersion) {} + + /** + * Returns whether the profiling cycle is controlled manually or based on the trace lifecycle. + * Defaults to {@link ProfileLifecycle#MANUAL}. + * + * @return the profile lifecycle + */ + @ApiStatus.Experimental + public @NotNull ProfileLifecycle getProfileLifecycle() { + return profileLifecycle; + } + + /** Sets the profiling lifecycle. */ + @ApiStatus.Experimental + public void setProfileLifecycle(final @NotNull ProfileLifecycle profileLifecycle) { + // TODO (when moved to SentryOptions): we should log a message if the user sets this to TRACE + // and tracing is disabled + this.profileLifecycle = profileLifecycle; + } + + @ApiStatus.Experimental + public @Nullable Double getProfileSessionSampleRate() { + return profileSessionSampleRate; + } + + @ApiStatus.Experimental + public void setProfileSessionSampleRate(final @Nullable Double profileSessionSampleRate) { + if (!SampleRateUtils.isValidContinuousProfilesSampleRate(profileSessionSampleRate)) { + throw new IllegalArgumentException( + "The value " + + profileSessionSampleRate + + " is not valid. Use values between 0.0 and 1.0."); + } + this.profileSessionSampleRate = profileSessionSampleRate; + } + + @ApiStatus.Experimental + public boolean isStartProfilerOnAppStart() { + return startProfilerOnAppStart; + } + + @ApiStatus.Experimental + public void setStartProfilerOnAppStart(boolean startProfilerOnAppStart) { + this.startProfilerOnAppStart = startProfilerOnAppStart; + } } diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index 7fba4da99d8..82fe9f5ce33 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 startProfileSession() { + Sentry.startProfileSession(); + } + + @Override + public void stopProfileSession() { + Sentry.stopProfileSession(); + } + + @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 9ca84df9a17..8b9bc1a790f 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 startProfileSession() { + scopes.startProfileSession(); + } + + @Override + public void stopProfileSession() { + scopes.stopProfileSession(); + } + @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 new file mode 100644 index 00000000000..68cd32813c0 --- /dev/null +++ b/sentry/src/main/java/io/sentry/IContinuousProfiler.java @@ -0,0 +1,24 @@ +package io.sentry; + +import io.sentry.protocol.SentryId; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** Used for performing operations when a transaction is started or ended. */ +@ApiStatus.Internal +public interface IContinuousProfiler { + boolean isRunning(); + + void startProfileSession( + final @NotNull ProfileLifecycle profileLifecycle, final @NotNull TracesSampler tracesSampler); + + void stopProfileSession(final @NotNull ProfileLifecycle profileLifecycle); + + /** Cancel the profiler and stops it. Used on SDK close. */ + void close(); + + void reevaluateSampling(); + + @NotNull + SentryId getProfilerId(); +} diff --git a/sentry/src/main/java/io/sentry/IScopes.java b/sentry/src/main/java/io/sentry/IScopes.java index c93f558ea57..9c356a5464e 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 startProfileSession(); + + void stopProfileSession(); + /** * 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 017b283d579..22389f2b6e5 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 1e429e2fea4..9b7c6afabab 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 cdde808ba5b..9ede59ba079 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 09773dc617c..bb2d356a1d5 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 0e319845d66..313df85882d 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.SdkVersion; import io.sentry.protocol.SentryException; @@ -69,34 +68,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 0fbb66412e4..1155e00b4ba 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 abf5ec5f6ae..a159be9182f 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 new file mode 100644 index 00000000000..2e788ad6d0a --- /dev/null +++ b/sentry/src/main/java/io/sentry/NoOpContinuousProfiler.java @@ -0,0 +1,39 @@ +package io.sentry; + +import io.sentry.protocol.SentryId; +import org.jetbrains.annotations.NotNull; + +public final class NoOpContinuousProfiler implements IContinuousProfiler { + + private static final NoOpContinuousProfiler instance = new NoOpContinuousProfiler(); + + private NoOpContinuousProfiler() {} + + public static NoOpContinuousProfiler getInstance() { + return instance; + } + + @Override + public void stopProfileSession(final @NotNull ProfileLifecycle profileLifecycle) {} + + @Override + public boolean isRunning() { + return false; + } + + @Override + public void startProfileSession( + final @NotNull ProfileLifecycle profileLifecycle, + final @NotNull TracesSampler tracesSampler) {} + + @Override + public void close() {} + + @Override + public void reevaluateSampling() {} + + @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 9b39ce77a6d..2d1505c5439 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 startProfileSession() {} + + @Override + public void stopProfileSession() {} + @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 a37a730b74c..455b936ff2f 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 startProfileSession() {} + + @Override + public void stopProfileSession() {} + @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 162b1fae5ab..4bee0008053 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 05bea4edfe9..871e2810542 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 00000000000..89d9293f5c2 --- /dev/null +++ b/sentry/src/main/java/io/sentry/ProfileChunk.java @@ -0,0 +1,336 @@ +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 double timestamp; + + 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<>(), + 0.0, + SentryOptions.empty()); + } + + public ProfileChunk( + final @NotNull SentryId profilerId, + final @NotNull SentryId chunkId, + final @NotNull File traceFile, + final @NotNull Map measurements, + final @NotNull Double timestamp, + 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"; + this.timestamp = timestamp; + } + + 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 double getTimestamp() { + return timestamp; + } + + 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; + private final double timestamp; + + public Builder( + final @NotNull SentryId profilerId, + final @NotNull SentryId chunkId, + final @NotNull Map measurements, + final @NotNull File traceFile, + final @NotNull SentryDate timestamp) { + this.profilerId = profilerId; + this.chunkId = chunkId; + this.measurements = new ConcurrentHashMap<>(measurements); + this.traceFile = traceFile; + this.timestamp = DateUtils.nanosToSeconds(timestamp.nanoTimestamp()); + } + + public ProfileChunk build(SentryOptions options) { + return new ProfileChunk(profilerId, chunkId, traceFile, measurements, timestamp, 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"; + public static final String TIMESTAMP = "timestamp"; + } + + @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); + } + writer.name(JsonKeys.TIMESTAMP).value(logger, timestamp); + 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; + case JsonKeys.TIMESTAMP: + Double timestamp = reader.nextDoubleOrNull(); + if (timestamp != null) { + data.timestamp = timestamp; + } + 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 00000000000..e4b411c279a --- /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/ProfileLifecycle.java b/sentry/src/main/java/io/sentry/ProfileLifecycle.java new file mode 100644 index 00000000000..42c26e5344b --- /dev/null +++ b/sentry/src/main/java/io/sentry/ProfileLifecycle.java @@ -0,0 +1,18 @@ +package io.sentry; + +/** + * Determines whether the profiling lifecycle is controlled manually or based on the trace + * lifecycle. + */ +public enum ProfileLifecycle { + /** + * Profiling is controlled manually. You must use the {@link Sentry#startProfileSession()} and + * {@link Sentry#stopProfileSession()} APIs to control the lifecycle of the profiler. + */ + MANUAL, + /** + * Profiling is automatically started when there is at least 1 sampled root span, and it's + * automatically stopped when there are none. + */ + TRACE +} diff --git a/sentry/src/main/java/io/sentry/Scope.java b/sentry/src/main/java/io/sentry/Scope.java index a3046dad28c..3cb7b5554dc 100644 --- a/sentry/src/main/java/io/sentry/Scope.java +++ b/sentry/src/main/java/io/sentry/Scope.java @@ -946,6 +946,8 @@ public SessionPair startSession() { if (session != null) { // Assumes session will NOT flush itself (Not passing any scopes to it) session.end(); + // Continuous profiler sample rate is reevaluated every time a session ends + options.getContinuousProfiler().reevaluateSampling(); } previousSession = session; @@ -1019,6 +1021,8 @@ public Session endSession() { try (final @NotNull ISentryLifecycleToken ignored = sessionLock.acquire()) { if (session != null) { session.end(); + // Continuous profiler sample rate is reevaluated every time a session ends + options.getContinuousProfiler().reevaluateSampling(); previousSession = session.clone(); session = null; } diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index f34b66680ee..f9718afbcd5 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() { @@ -405,7 +405,8 @@ public void close(final boolean isRestarting) { configureScope(ScopeType.ISOLATION, scope -> scope.clear()); getOptions().getBackpressureMonitor().close(); getOptions().getTransactionProfiler().close(); - getOptions().getTransactionPerformanceCollector().close(); + getOptions().getContinuousProfiler().close(); + getOptions().getCompositePerformanceCollector().close(); final @NotNull ISentryExecutorService executorService = getOptions().getExecutorService(); if (isRestarting) { executorService.submit( @@ -810,6 +811,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, @@ -871,22 +901,34 @@ 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 - if (samplingDecision.getSampled() && samplingDecision.getProfileSampled()) { - final ITransactionProfiler transactionProfiler = getOptions().getTransactionProfiler(); - // If the profiler is not running, we start and bind it here. - if (!transactionProfiler.isRunning()) { - transactionProfiler.start(); - transactionProfiler.bindTransaction(transaction); - } else if (transactionOptions.isAppStartTransaction()) { - // If the profiler is running and the current transaction is the app start, we bind it. - transactionProfiler.bindTransaction(transaction); + if (samplingDecision.getSampled()) { + // If transaction profiler is sampled, let's start it + if (samplingDecision.getProfileSampled()) { + final ITransactionProfiler transactionProfiler = getOptions().getTransactionProfiler(); + // If the profiler is not running, we start and bind it here. + if (!transactionProfiler.isRunning()) { + transactionProfiler.start(); + transactionProfiler.bindTransaction(transaction); + } else if (transactionOptions.isAppStartTransaction()) { + // If the profiler is running and the current transaction is the app start, we bind it. + transactionProfiler.bindTransaction(transaction); + } + } + + // If continuous profiling is enabled in trace mode, let's start it. Profiler will sample on + // its own. + if (getOptions().isContinuousProfilingEnabled() + && getOptions().getProfileLifecycle() == ProfileLifecycle.TRACE) { + getOptions() + .getContinuousProfiler() + .startProfileSession(ProfileLifecycle.TRACE, getOptions().getInternalTracesSampler()); } } } @@ -908,6 +950,53 @@ public void flush(long timeoutMillis) { return getCombinedScopeView().getPropagationContext().getSampleRand(); } + @Override + public void startProfileSession() { + if (getOptions().isContinuousProfilingEnabled()) { + if (getOptions().getProfileLifecycle() != ProfileLifecycle.MANUAL) { + getOptions() + .getLogger() + .log( + SentryLevel.WARNING, + "Profiling lifecycle is %s. Profiling cannot be started manually.", + getOptions().getProfileLifecycle().name()); + return; + } + getOptions() + .getContinuousProfiler() + .startProfileSession(ProfileLifecycle.MANUAL, getOptions().getInternalTracesSampler()); + } else if (getOptions().isProfilingEnabled()) { + getOptions() + .getLogger() + .log( + SentryLevel.WARNING, + "Continuous Profiling is not enabled. Set profilesSampleRate and profilesSampler to null to enable it."); + } + } + + @Override + public void stopProfileSession() { + if (getOptions().isContinuousProfilingEnabled()) { + if (getOptions().getProfileLifecycle() != ProfileLifecycle.MANUAL) { + getOptions() + .getLogger() + .log( + SentryLevel.WARNING, + "Profiling lifecycle is %s. Profiling cannot be stopped manually.", + getOptions().getProfileLifecycle().name()); + return; + } + getOptions().getLogger().log(SentryLevel.DEBUG, "Stopped continuous Profiling."); + getOptions().getContinuousProfiler().stopProfileSession(ProfileLifecycle.MANUAL); + } 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 d5d143af52b..aeaf193b9be 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 startProfileSession() { + Sentry.startProfileSession(); + } + + @Override + public void stopProfileSession() { + Sentry.stopProfileSession(); + } + @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 82459410a48..ab27f2130e8 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -415,10 +415,12 @@ private static void handleAppStartProfilingConfig( try { // We always delete the config file for app start profiling FileUtils.deleteRecursively(appStartProfilingConfigFile); - if (!options.isEnableAppStartProfiling()) { + if (!options.isEnableAppStartProfiling() && !options.isStartProfilerOnAppStart()) { return; } - if (!options.isTracingEnabled()) { + // isStartProfilerOnAppStart doesn't need tracing, as it can be started/stopped + // manually + if (!options.isStartProfilerOnAppStart() && !options.isTracingEnabled()) { options .getLogger() .log( @@ -427,8 +429,13 @@ private static void handleAppStartProfilingConfig( return; } if (appStartProfilingConfigFile.createNewFile()) { + // If old app start profiling is false, it means the transaction will not be + // sampled, but we create the file anyway to allow continuous profiling on app + // start final @NotNull TracesSamplingDecision appStartSamplingDecision = - sampleAppStartProfiling(options); + options.isEnableAppStartProfiling() + ? sampleAppStartProfiling(options) + : new TracesSamplingDecision(false); final @NotNull SentryAppStartProfilingOptions appStartProfilingOptions = new SentryAppStartProfilingOptions(options, appStartSamplingDecision); try (final OutputStream outputStream = @@ -566,7 +573,8 @@ private static void initConfigurations(final @NotNull SentryOptions options) { } final String profilingTracesDirPath = options.getProfilingTracesDirPath(); - if (options.isProfilingEnabled() && profilingTracesDirPath != null) { + if ((options.isProfilingEnabled() || options.isContinuousProfilingEnabled()) + && profilingTracesDirPath != null) { final File profilingTracesDir = new File(profilingTracesDirPath); profilingTracesDir.mkdirs(); @@ -1104,6 +1112,18 @@ public static void endSession() { return getCurrentScopes().startTransaction(transactionContext, transactionOptions); } + /** Starts the continuous profiler, if enabled. */ + @ApiStatus.Experimental + public static void startProfileSession() { + getCurrentScopes().startProfileSession(); + } + + /** Stops the continuous profiler, if enabled. */ + @ApiStatus.Experimental + public static void stopProfileSession() { + getCurrentScopes().stopProfileSession(); + } + /** * 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 a9828792d77..ccacccf9c0b 100644 --- a/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java +++ b/sentry/src/main/java/io/sentry/SentryAppStartProfilingOptions.java @@ -1,5 +1,6 @@ package io.sentry; +import io.sentry.util.SentryRandom; import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; import java.util.Map; @@ -18,7 +19,12 @@ public final class SentryAppStartProfilingOptions implements JsonUnknown, JsonSe @Nullable Double traceSampleRate; @Nullable String profilingTracesDirPath; boolean isProfilingEnabled; + boolean isContinuousProfilingEnabled; int profilingTracesHz; + boolean continuousProfileSampled; + boolean isEnableAppStartProfiling; + boolean isStartProfilerOnAppStart; + @NotNull ProfileLifecycle profileLifecycle; private @Nullable Map unknown; @@ -28,9 +34,14 @@ public SentryAppStartProfilingOptions() { traceSampleRate = null; profileSampled = false; profileSampleRate = null; + continuousProfileSampled = false; profilingTracesDirPath = null; isProfilingEnabled = false; + isContinuousProfilingEnabled = false; + profileLifecycle = ProfileLifecycle.MANUAL; profilingTracesHz = 0; + isEnableAppStartProfiling = true; + isStartProfilerOnAppStart = false; } SentryAppStartProfilingOptions( @@ -40,9 +51,17 @@ public SentryAppStartProfilingOptions() { traceSampleRate = samplingDecision.getSampleRate(); profileSampled = samplingDecision.getProfileSampled(); profileSampleRate = samplingDecision.getProfileSampleRate(); + continuousProfileSampled = + options + .getInternalTracesSampler() + .sampleSessionProfile(SentryRandom.current().nextDouble()); profilingTracesDirPath = options.getProfilingTracesDirPath(); isProfilingEnabled = options.isProfilingEnabled(); + isContinuousProfilingEnabled = options.isContinuousProfilingEnabled(); + profileLifecycle = options.getProfileLifecycle(); profilingTracesHz = options.getProfilingTracesHz(); + isEnableAppStartProfiling = options.isEnableAppStartProfiling(); + isStartProfilerOnAppStart = options.isStartProfilerOnAppStart(); } public void setProfileSampled(final boolean profileSampled) { @@ -53,6 +72,22 @@ public boolean isProfileSampled() { return profileSampled; } + public void setContinuousProfileSampled(boolean continuousProfileSampled) { + this.continuousProfileSampled = continuousProfileSampled; + } + + public boolean isContinuousProfileSampled() { + return continuousProfileSampled; + } + + public void setProfileLifecycle(final @NotNull ProfileLifecycle profileLifecycle) { + this.profileLifecycle = profileLifecycle; + } + + public @NotNull ProfileLifecycle getProfileLifecycle() { + return profileLifecycle; + } + public void setProfileSampleRate(final @Nullable Double profileSampleRate) { this.profileSampleRate = profileSampleRate; } @@ -93,6 +128,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; } @@ -101,16 +144,37 @@ public int getProfilingTracesHz() { return profilingTracesHz; } + public void setEnableAppStartProfiling(final boolean enableAppStartProfiling) { + isEnableAppStartProfiling = enableAppStartProfiling; + } + + public boolean isEnableAppStartProfiling() { + return isEnableAppStartProfiling; + } + + public void setStartProfilerOnAppStart(final boolean startProfilerOnAppStart) { + isStartProfilerOnAppStart = startProfilerOnAppStart; + } + + public boolean isStartProfilerOnAppStart() { + return isStartProfilerOnAppStart; + } + // JsonSerializable public static final class JsonKeys { public static final String PROFILE_SAMPLED = "profile_sampled"; public static final String PROFILE_SAMPLE_RATE = "profile_sample_rate"; + public static final String CONTINUOUS_PROFILE_SAMPLED = "continuous_profile_sampled"; public static final String TRACE_SAMPLED = "trace_sampled"; 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 PROFILE_LIFECYCLE = "profile_lifecycle"; public static final String PROFILING_TRACES_HZ = "profiling_traces_hz"; + public static final String IS_ENABLE_APP_START_PROFILING = "is_enable_app_start_profiling"; + public static final String IS_START_PROFILER_ON_APP_START = "is_start_profiler_on_app_start"; } @Override @@ -119,11 +183,18 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger writer.beginObject(); writer.name(JsonKeys.PROFILE_SAMPLED).value(logger, profileSampled); writer.name(JsonKeys.PROFILE_SAMPLE_RATE).value(logger, profileSampleRate); + writer.name(JsonKeys.CONTINUOUS_PROFILE_SAMPLED).value(logger, continuousProfileSampled); writer.name(JsonKeys.TRACE_SAMPLED).value(logger, traceSampled); 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.PROFILE_LIFECYCLE).value(logger, profileLifecycle.name()); writer.name(JsonKeys.PROFILING_TRACES_HZ).value(logger, profilingTracesHz); + writer.name(JsonKeys.IS_ENABLE_APP_START_PROFILING).value(logger, isEnableAppStartProfiling); + writer.name(JsonKeys.IS_START_PROFILER_ON_APP_START).value(logger, isStartProfilerOnAppStart); if (unknown != null) { for (String key : unknown.keySet()) { @@ -160,47 +231,83 @@ public static final class Deserializer final String nextName = reader.nextName(); switch (nextName) { case JsonKeys.PROFILE_SAMPLED: - Boolean profileSampled = reader.nextBooleanOrNull(); + @Nullable Boolean profileSampled = reader.nextBooleanOrNull(); if (profileSampled != null) { options.profileSampled = profileSampled; } break; case JsonKeys.PROFILE_SAMPLE_RATE: - Double profileSampleRate = reader.nextDoubleOrNull(); + @Nullable Double profileSampleRate = reader.nextDoubleOrNull(); if (profileSampleRate != null) { options.profileSampleRate = profileSampleRate; } break; + case JsonKeys.CONTINUOUS_PROFILE_SAMPLED: + @Nullable Boolean continuousProfileSampled = reader.nextBooleanOrNull(); + if (continuousProfileSampled != null) { + options.continuousProfileSampled = continuousProfileSampled; + } + break; case JsonKeys.TRACE_SAMPLED: - Boolean traceSampled = reader.nextBooleanOrNull(); + @Nullable Boolean traceSampled = reader.nextBooleanOrNull(); if (traceSampled != null) { options.traceSampled = traceSampled; } break; case JsonKeys.TRACE_SAMPLE_RATE: - Double traceSampleRate = reader.nextDoubleOrNull(); + @Nullable Double traceSampleRate = reader.nextDoubleOrNull(); if (traceSampleRate != null) { options.traceSampleRate = traceSampleRate; } break; case JsonKeys.PROFILING_TRACES_DIR_PATH: - String profilingTracesDirPath = reader.nextStringOrNull(); + @Nullable String profilingTracesDirPath = reader.nextStringOrNull(); if (profilingTracesDirPath != null) { options.profilingTracesDirPath = profilingTracesDirPath; } break; case JsonKeys.IS_PROFILING_ENABLED: - Boolean isProfilingEnabled = reader.nextBooleanOrNull(); + @Nullable Boolean isProfilingEnabled = reader.nextBooleanOrNull(); if (isProfilingEnabled != null) { options.isProfilingEnabled = isProfilingEnabled; } break; + case JsonKeys.IS_CONTINUOUS_PROFILING_ENABLED: + @Nullable Boolean isContinuousProfilingEnabled = reader.nextBooleanOrNull(); + if (isContinuousProfilingEnabled != null) { + options.isContinuousProfilingEnabled = isContinuousProfilingEnabled; + } + break; + case JsonKeys.PROFILE_LIFECYCLE: + @Nullable String profileLifecycle = reader.nextStringOrNull(); + if (profileLifecycle != null) { + try { + options.profileLifecycle = ProfileLifecycle.valueOf(profileLifecycle); + } catch (IllegalArgumentException e) { + logger.log( + SentryLevel.ERROR, + "Error when deserializing ProfileLifecycle: " + profileLifecycle); + } + } + break; case JsonKeys.PROFILING_TRACES_HZ: - Integer profilingTracesHz = reader.nextIntegerOrNull(); + @Nullable Integer profilingTracesHz = reader.nextIntegerOrNull(); if (profilingTracesHz != null) { options.profilingTracesHz = profilingTracesHz; } break; + case JsonKeys.IS_ENABLE_APP_START_PROFILING: + @Nullable Boolean isEnableAppStartProfiling = reader.nextBooleanOrNull(); + if (isEnableAppStartProfiling != null) { + options.isEnableAppStartProfiling = isEnableAppStartProfiling; + } + break; + case JsonKeys.IS_START_PROFILER_ON_APP_START: + @Nullable Boolean isStartProfilerOnAppStart = reader.nextBooleanOrNull(); + if (isStartProfilerOnAppStart != null) { + options.isStartProfilerOnAppStart = isStartProfilerOnAppStart; + } + break; default: if (unknown == null) { unknown = new ConcurrentHashMap<>(); diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 277be87aeef..c98970306b7 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; @@ -483,8 +484,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) { @@ -893,6 +893,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 2d5f6484b32..62892e3ed44 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 85acc0aaddd..068c37ab1f1 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 1ddb12f0772..14b09811eb8 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -367,9 +367,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. */ @@ -441,8 +444,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; @@ -492,7 +495,10 @@ public class SentryOptions { private boolean enableBackpressureHandling = true; - /** Whether to profile app launches, depending on profilesSampler or profilesSampleRate. */ + /** + * Whether to profile app launches, depending on profilesSampler, profilesSampleRate or + * continuousProfilesSampleRate. + */ private boolean enableAppStartProfiling = false; private @NotNull ISpanFactory spanFactory = NoOpSpanFactory.getInstance(); @@ -1786,14 +1792,51 @@ public void setTransactionProfiler(final @Nullable ITransactionProfiler transact } } + /** + * Returns the continuous profiler. + * + * @return the continuous profiler. + */ + @ApiStatus.Experimental + 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 + */ + @ApiStatus.Experimental + 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. * * @return if profiling is enabled for transactions. */ public boolean isProfilingEnabled() { - return (getProfilesSampleRate() != null && getProfilesSampleRate() > 0) - || getProfilesSampler() != null; + return (profilesSampleRate != null && profilesSampleRate > 0) || profilesSampler != 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 profilesSampleRate == null + && profilesSampler == null + && experimental.getProfileSessionSampleRate() != null + && experimental.getProfileSessionSampleRate() > 0; } /** @@ -1840,6 +1883,37 @@ public void setProfilesSampleRate(final @Nullable Double profilesSampleRate) { this.profilesSampleRate = profilesSampleRate; } + /** + * Returns the session sample rate. Default is null (disabled). ProfilesSampleRate takes + * precedence over this. To enable continuous profiling, don't set profilesSampleRate or + * profilesSampler, or set them to null. + * + * @return the sample rate + */ + @ApiStatus.Experimental + public @Nullable Double getProfileSessionSampleRate() { + return experimental.getProfileSessionSampleRate(); + } + + /** + * Returns whether the profiling lifecycle is controlled manually or based on the trace lifecycle. + * Defaults to {@link ProfileLifecycle#MANUAL}. + * + * @return the profile lifecycle + */ + @ApiStatus.Experimental + public @NotNull ProfileLifecycle getProfileLifecycle() { + return experimental.getProfileLifecycle(); + } + + /** + * Whether profiling can automatically be started as early as possible during the app lifecycle. + */ + @ApiStatus.Experimental + public boolean isStartProfilerOnAppStart() { + return experimental.isStartProfilerOnAppStart(); + } + /** * Returns the profiling traces dir. path if set * @@ -2118,24 +2192,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; } /** @@ -2237,17 +2311,19 @@ public void setEnablePrettySerializationOutput(boolean enablePrettySerialization } /** - * Whether to profile app launches, depending on profilesSampler or profilesSampleRate. Depends on - * {@link SentryOptions#isProfilingEnabled()} + * Whether to profile app launches, depending on profilesSampler, profilesSampleRate or + * continuousProfilesSampleRate. Depends on {@link SentryOptions#isProfilingEnabled()} and {@link + * SentryOptions#isContinuousProfilingEnabled()} * * @return true if app launches should be profiled. */ public boolean isEnableAppStartProfiling() { - return isProfilingEnabled() && enableAppStartProfiling; + return (isProfilingEnabled() || isContinuousProfilingEnabled()) && enableAppStartProfiling; } /** - * Whether to profile app launches, depending on profilesSampler or profilesSampleRate. + * Whether to profile app launches, depending on profilesSampler, profilesSampleRate or + * continuousProfilesSampleRate. * * @param enableAppStartProfiling true if app launches should be profiled. */ diff --git a/sentry/src/main/java/io/sentry/SentryTracer.java b/sentry/src/main/java/io/sentry/SentryTracer.java index af5459a1a37..18ef57fe899 100644 --- a/sentry/src/main/java/io/sentry/SentryTracer.java +++ b/sentry/src/main/java/io/sentry/SentryTracer.java @@ -8,6 +8,7 @@ import io.sentry.util.CollectionUtils; import io.sentry.util.Objects; import io.sentry.util.SpanUtils; +import io.sentry.util.thread.IThreadChecker; import java.util.List; import java.util.ListIterator; import java.util.Map; @@ -49,7 +50,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) { @@ -67,7 +68,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"); @@ -76,14 +77,20 @@ 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; + final @NotNull SentryId continuousProfilerId = + scopes.getOptions().getContinuousProfiler().getProfilerId(); + if (!continuousProfilerId.equals(SentryId.EMPTY_ID) && Boolean.TRUE.equals(isSampled())) { + 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 @@ -215,8 +222,8 @@ public void finish( finishedCallback.execute(this); } - if (transactionPerformanceCollector != null) { - performanceCollectionData.set(transactionPerformanceCollector.stop(this)); + if (compositePerformanceCollector != null) { + performanceCollectionData.set(compositePerformanceCollector.stop(this)); } }); @@ -234,6 +241,10 @@ public void finish( .getTransactionProfiler() .onTransactionFinish(this, performanceCollectionData.get(), scopes.getOptions()); } + if (scopes.getOptions().isContinuousProfilingEnabled() + && scopes.getOptions().getProfileLifecycle() == ProfileLifecycle.TRACE) { + scopes.getOptions().getContinuousProfiler().stopProfileSession(ProfileLifecycle.TRACE); + } if (performanceCollectionData.get() != null) { performanceCollectionData.get().clear(); } @@ -464,8 +475,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) { @@ -490,8 +501,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) { @@ -508,16 +519,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) && Boolean.TRUE.equals(span.isSampled())) { + 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 05f9aa25a5a..2999ea4a2b8 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.status = status; this.origin = origin; setSamplingDecision(samplingDecision); + 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()); } /** @@ -114,6 +120,16 @@ public SpanContext(final @NotNull SpanContext spanContext) { 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 ffe2414af39..c4329f6dcad 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/TracesSampler.java b/sentry/src/main/java/io/sentry/TracesSampler.java index b3da8d63ccf..5430b9242ac 100644 --- a/sentry/src/main/java/io/sentry/TracesSampler.java +++ b/sentry/src/main/java/io/sentry/TracesSampler.java @@ -82,6 +82,11 @@ public TracesSamplingDecision sample(final @NotNull SamplingContext samplingCont return new TracesSamplingDecision(false, null, sampleRand, false, null); } + public boolean sampleSessionProfile(final double sampleRand) { + final @Nullable Double sampling = options.getProfileSessionSampleRate(); + return sampling != null && sample(sampling, sampleRand); + } + private boolean sample(final @NotNull Double sampleRate, final @NotNull Double sampleRand) { return !(sampleRate < sampleRand); } diff --git a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java index b4d4574abae..fae019f464b 100644 --- a/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java +++ b/sentry/src/main/java/io/sentry/clientreport/ClientReportRecorder.java @@ -165,6 +165,9 @@ private DataCategory categoryFromItemType(SentryItemType itemType) { if (SentryItemType.Profile.equals(itemType)) { return DataCategory.Profile; } + if (SentryItemType.ProfileChunk.equals(itemType)) { + return DataCategory.ProfileChunk; + } if (SentryItemType.Attachment.equals(itemType)) { return DataCategory.Attachment; } diff --git a/sentry/src/main/java/io/sentry/profilemeasurements/ProfileMeasurementValue.java b/sentry/src/main/java/io/sentry/profilemeasurements/ProfileMeasurementValue.java index b0cebf5439d..12972d36e44 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 123436cfae4..375ad0f05bc 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; @@ -55,6 +56,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 if (Spring.TYPE.equals(entry.getKey()) && value instanceof Spring) { @@ -80,6 +83,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); } @@ -305,6 +317,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 458c4de6311..85ce67ab8e0 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 684001843a8..d2a9e5140b8 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/transport/RateLimiter.java b/sentry/src/main/java/io/sentry/transport/RateLimiter.java index 4e667e97a7f..4231aa65d23 100644 --- a/sentry/src/main/java/io/sentry/transport/RateLimiter.java +++ b/sentry/src/main/java/io/sentry/transport/RateLimiter.java @@ -191,6 +191,8 @@ private boolean isRetryAfter(final @NotNull String itemType) { return DataCategory.Attachment; case "profile": return DataCategory.Profile; + case "profile_chunk": + return DataCategory.ProfileChunk; case "transaction": return DataCategory.Transaction; case "check_in": diff --git a/sentry/src/main/java/io/sentry/util/SampleRateUtils.java b/sentry/src/main/java/io/sentry/util/SampleRateUtils.java index 225ce58a3b5..cb0c4557c1e 100644 --- a/sentry/src/main/java/io/sentry/util/SampleRateUtils.java +++ b/sentry/src/main/java/io/sentry/util/SampleRateUtils.java @@ -25,6 +25,10 @@ public static boolean isValidProfilesSampleRate(@Nullable Double profilesSampleR return isValidRate(profilesSampleRate, true); } + public static boolean isValidContinuousProfilesSampleRate(@Nullable Double profilesSampleRate) { + return isValidRate(profilesSampleRate, true); + } + public static @NotNull Double backfilledSampleRand( final @Nullable Double sampleRand, final @Nullable Double sampleRate, 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 81af056e711..deea360f8c5 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 b1497d17e7d..f80a9967859 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 bfa8aac139e..2f9b6fc1d2d 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 db113fa009c..20d6b693677 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 60005935c94..fe9dd6039dc 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 76e79b0e48f..db63ff25b34 100644 --- a/sentry/src/test/java/io/sentry/HubAdapterTest.kt +++ b/sentry/src/test/java/io/sentry/HubAdapterTest.kt @@ -219,6 +219,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() @@ -263,4 +269,14 @@ class HubAdapterTest { HubAdapter.getInstance().reportFullyDisplayed() verify(scopes).reportFullyDisplayed() } + + @Test fun `startProfileSession calls Hub`() { + HubAdapter.getInstance().startProfileSession() + verify(scopes).startProfileSession() + } + + @Test fun `stopProfileSession calls Hub`() { + HubAdapter.getInstance().stopProfileSession() + verify(scopes).stopProfileSession() + } } diff --git a/sentry/src/test/java/io/sentry/JavaMemoryCollectorTest.kt b/sentry/src/test/java/io/sentry/JavaMemoryCollectorTest.kt index e2914edff69..7e076bba78d 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 8e3140faa11..86f5c70bbcb 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,208 @@ 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(), 5.3, 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(5.3, element["timestamp"] as Double) + 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", + "timestamp":"5.3", + "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(5.3, profileChunk.timestamp) + 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 +1071,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 +1105,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 +1170,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")) @@ -1003,16 +1232,19 @@ class JsonSerializerTest { fun `serializing SentryAppStartProfilingOptions`() { 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}" - + val expected = "{\"profile_sampled\":true,\"profile_sample_rate\":0.8,\"continuous_profile_sampled\":true," + + "\"trace_sampled\":false,\"trace_sample_rate\":0.1,\"profiling_traces_dir_path\":null,\"is_profiling_enabled\":false," + + "\"is_continuous_profiling_enabled\":false,\"profile_lifecycle\":\"TRACE\",\"profiling_traces_hz\":65," + + "\"is_enable_app_start_profiling\":false,\"is_start_profiler_on_app_start\":true}" assertEquals(expected, actual) } @Test fun `deserializing SentryAppStartProfilingOptions`() { val jsonAppStartProfilingOptions = "{\"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}" + ":false,\"trace_sample_rate\":0.1,\"profiling_traces_dir_path\":null,\"is_profiling_enabled\":false," + + "\"profile_lifecycle\":\"TRACE\",\"profiling_traces_hz\":65,\"continuous_profile_sampled\":true," + + "\"is_enable_app_start_profiling\":false,\"is_start_profiler_on_app_start\":true}" val actual = fixture.serializer.deserialize(StringReader(jsonAppStartProfilingOptions), SentryAppStartProfilingOptions::class.java) assertNotNull(actual) @@ -1020,9 +1252,14 @@ class JsonSerializerTest { assertEquals(appStartProfilingOptions.traceSampleRate, actual.traceSampleRate) assertEquals(appStartProfilingOptions.profileSampled, actual.profileSampled) assertEquals(appStartProfilingOptions.profileSampleRate, actual.profileSampleRate) + assertEquals(appStartProfilingOptions.continuousProfileSampled, actual.isContinuousProfileSampled) assertEquals(appStartProfilingOptions.isProfilingEnabled, actual.isProfilingEnabled) + assertEquals(appStartProfilingOptions.isContinuousProfilingEnabled, actual.isContinuousProfilingEnabled) assertEquals(appStartProfilingOptions.profilingTracesHz, actual.profilingTracesHz) assertEquals(appStartProfilingOptions.profilingTracesDirPath, actual.profilingTracesDirPath) + assertEquals(appStartProfilingOptions.profileLifecycle, actual.profileLifecycle) + assertEquals(appStartProfilingOptions.isEnableAppStartProfiling, actual.isEnableAppStartProfiling) + assertEquals(appStartProfilingOptions.isStartProfilerOnAppStart, actual.isStartProfilerOnAppStart) assertNull(actual.unknown) } @@ -1321,10 +1558,15 @@ class JsonSerializerTest { private val appStartProfilingOptions = SentryAppStartProfilingOptions().apply { traceSampled = false traceSampleRate = 0.1 + continuousProfileSampled = true profileSampled = true profileSampleRate = 0.8 isProfilingEnabled = false + isContinuousProfilingEnabled = false profilingTracesHz = 65 + profileLifecycle = ProfileLifecycle.TRACE + isEnableAppStartProfiling = false + isStartProfilerOnAppStart = true } private fun createSpan(): ISpan { diff --git a/sentry/src/test/java/io/sentry/NoOpContinuousProfilerTest.kt b/sentry/src/test/java/io/sentry/NoOpContinuousProfilerTest.kt new file mode 100644 index 00000000000..081c72169d6 --- /dev/null +++ b/sentry/src/test/java/io/sentry/NoOpContinuousProfilerTest.kt @@ -0,0 +1,38 @@ +package io.sentry + +import io.sentry.protocol.SentryId +import org.mockito.kotlin.mock +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse + +class NoOpContinuousProfilerTest { + private var profiler = NoOpContinuousProfiler.getInstance() + + @Test + fun `start does not throw`() = + profiler.startProfileSession(mock(), mock()) + + @Test + fun `stop does not throw`() = + profiler.stopProfileSession(mock()) + + @Test + fun `isRunning returns false`() { + assertFalse(profiler.isRunning) + } + + @Test + fun `close does not throw`() = + profiler.close() + + @Test + fun `getProfilerId returns Empty SentryId`() { + assertEquals(profiler.profilerId, SentryId.EMPTY_ID) + } + + @Test + fun `reevaluateSampling does not throw`() { + profiler.reevaluateSampling() + } +} diff --git a/sentry/src/test/java/io/sentry/NoOpHubTest.kt b/sentry/src/test/java/io/sentry/NoOpHubTest.kt index f20257482d8..fdf61859700 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 `startProfileSession doesnt throw`() = sut.startProfileSession() + + @Test + fun `stopProfileSession doesnt throw`() = sut.stopProfileSession() } diff --git a/sentry/src/test/java/io/sentry/NoOpSentryClientTest.kt b/sentry/src/test/java/io/sentry/NoOpSentryClientTest.kt index 919ce5f083c..8f8d76eba26 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 9136494ddf4..34ecd746255 100644 --- a/sentry/src/test/java/io/sentry/OutboxSenderTest.kt +++ b/sentry/src/test/java/io/sentry/OutboxSenderTest.kt @@ -39,6 +39,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 e105e105c66..76866b09e6e 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/ScopeTest.kt b/sentry/src/test/java/io/sentry/ScopeTest.kt index 6f29ff54b80..5645d582a36 100644 --- a/sentry/src/test/java/io/sentry/ScopeTest.kt +++ b/sentry/src/test/java/io/sentry/ScopeTest.kt @@ -10,6 +10,7 @@ import org.mockito.kotlin.argThat import org.mockito.kotlin.check import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -391,6 +392,57 @@ class ScopeTest { assertNull(session) } + @Test + fun `Starting a session multiple times reevaluates profileSessionSampleRate`() { + val profiler = mock() + val options = SentryOptions().apply { + release = "0.0.1" + setContinuousProfiler(profiler) + experimental.profileSessionSampleRate = 1.0 + } + + val scope = Scope(options) + // The first time a session is started, sample rate is not reevaluated, as there's no need + scope.startSession() + verify(profiler, never()).reevaluateSampling() + // The second time a session is started, sample rate is reevaluated + scope.startSession() + verify(profiler).reevaluateSampling() + // Every time a session is started with an already running one, sample rate is reevaluated + scope.startSession() + verify(profiler, times(2)).reevaluateSampling() + } + + @Test + fun `Scope ends a session and reevaluates profileSessionSampleRate`() { + val profiler = mock() + val options = SentryOptions().apply { + release = "0.0.1" + setContinuousProfiler(profiler) + experimental.profileSessionSampleRate = 1.0 + } + + val scope = Scope(options) + scope.startSession() + verify(profiler, never()).reevaluateSampling() + scope.endSession() + verify(profiler).reevaluateSampling() + } + + @Test + fun `Scope ends a session and does not reevaluate profileSessionSampleRate if none exist`() { + val profiler = mock() + val options = SentryOptions().apply { + release = "0.0.1" + setContinuousProfiler(profiler) + experimental.profileSessionSampleRate = 1.0 + } + + val scope = Scope(options) + scope.endSession() + verify(profiler, never()).reevaluateSampling() + } + @Test fun `withSession returns a callback with the current Session`() { val options = SentryOptions().apply { diff --git a/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt b/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt index ea274d438b0..d68a77d4b0f 100644 --- a/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesAdapterTest.kt @@ -219,6 +219,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() @@ -263,4 +269,14 @@ class ScopesAdapterTest { ScopesAdapter.getInstance().reportFullyDisplayed() verify(scopes).reportFullyDisplayed() } + + @Test fun `startProfileSession calls Scopes`() { + ScopesAdapter.getInstance().startProfileSession() + verify(scopes).startProfileSession() + } + + @Test fun `stopProfileSession calls Scopes`() { + ScopesAdapter.getInstance().stopProfileSession() + verify(scopes).stopProfileSession() + } } diff --git a/sentry/src/test/java/io/sentry/ScopesTest.kt b/sentry/src/test/java/io/sentry/ScopesTest.kt index 23d2dcdd94a..32d76cec684 100644 --- a/sentry/src/test/java/io/sentry/ScopesTest.kt +++ b/sentry/src/test/java/io/sentry/ScopesTest.kt @@ -55,6 +55,7 @@ class ScopesTest { private lateinit var file: File private lateinit var profilingTraceFile: File + private val mockProfiler = spy(NoOpContinuousProfiler.getInstance()) @BeforeTest fun `set up`() { @@ -782,6 +783,8 @@ class ScopesTest { } } + //endregion + //region captureCheckIn tests @Test @@ -1586,6 +1589,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 @@ -1759,14 +1808,17 @@ 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 backpressureMonitorMock = 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) + experimental.profileSessionSampleRate = 1.0 backpressureMonitor = backpressureMonitorMock } val sut = createScopes(options) @@ -1774,6 +1826,7 @@ class ScopesTest { verify(backpressureMonitorMock).close() verify(executor).close(any()) verify(profiler).close() + verify(continuousProfiler).close() verify(performanceCollector).close() } @@ -1833,6 +1886,49 @@ class ScopesTest { val transaction = scopes.startTransaction(TransactionContext("name", "op", TracesSamplingDecision(true))) assertTrue(transaction is NoOpTransaction) } + + @Test + fun `when startTransaction, trace profile session is started`() { + val scopes = generateScopes { + it.tracesSampleRate = 1.0 + it.setContinuousProfiler(mockProfiler) + it.experimental.profileSessionSampleRate = 1.0 + it.experimental.profileLifecycle = ProfileLifecycle.TRACE + } + + val transaction = scopes.startTransaction("name", "op") + assertTrue(transaction.isSampled!!) + verify(mockProfiler).startProfileSession(eq(ProfileLifecycle.TRACE), any()) + } + + @Test + fun `when startTransaction, manual profile session is not started`() { + val scopes = generateScopes { + it.tracesSampleRate = 1.0 + it.setContinuousProfiler(mockProfiler) + it.experimental.profileSessionSampleRate = 1.0 + it.experimental.profileLifecycle = ProfileLifecycle.MANUAL + } + + val transaction = scopes.startTransaction("name", "op") + assertTrue(transaction.isSampled!!) + verify(mockProfiler, never()).startProfileSession(any(), any()) + } + + @Test + fun `when startTransaction not sampled, trace profile session is not started`() { + val scopes = generateScopes { + // If transaction is not sampled, profiler should not start + it.tracesSampleRate = 0.0 + it.setContinuousProfiler(mockProfiler) + it.experimental.profileSessionSampleRate = 1.0 + it.experimental.profileLifecycle = ProfileLifecycle.TRACE + } + val transaction = scopes.startTransaction("name", "op") + transaction.spanContext.setSampled(false, false) + assertFalse(transaction.isSampled!!) + verify(mockProfiler, never()).startProfileSession(any(), any()) + } //endregion //region getSpan tests @@ -2141,6 +2237,96 @@ class ScopesTest { assertEquals("other.span.origin", transaction.spanContext.origin) } + //region profileSession + + @Test + fun `startProfileSession starts the continuous profiler`() { + val profiler = mock() + val scopes = generateScopes { + it.setContinuousProfiler(profiler) + it.experimental.profileSessionSampleRate = 1.0 + } + scopes.startProfileSession() + verify(profiler).startProfileSession(eq(ProfileLifecycle.MANUAL), any()) + } + + @Test + fun `startProfileSession logs instructions if continuous profiling is disabled`() { + val profiler = mock() + val logger = mock() + val scopes = generateScopes { + it.setContinuousProfiler(profiler) + it.experimental.profileSessionSampleRate = 1.0 + it.profilesSampleRate = 1.0 + it.setLogger(logger) + it.isDebug = true + } + scopes.startProfileSession() + verify(profiler, never()).startProfileSession(eq(ProfileLifecycle.MANUAL), any()) + verify(logger).log(eq(SentryLevel.WARNING), eq("Continuous Profiling is not enabled. Set profilesSampleRate and profilesSampler to null to enable it.")) + } + + @Test + fun `startProfileSession is ignored on trace lifecycle`() { + val profiler = mock() + val logger = mock() + val scopes = generateScopes { + it.setContinuousProfiler(profiler) + it.experimental.profileSessionSampleRate = 1.0 + it.experimental.profileLifecycle = ProfileLifecycle.TRACE + it.setLogger(logger) + it.isDebug = true + } + scopes.startProfileSession() + verify(logger).log(eq(SentryLevel.WARNING), eq("Profiling lifecycle is %s. Profiling cannot be started manually."), eq(ProfileLifecycle.TRACE.name)) + verify(profiler, never()).startProfileSession(any(), any()) + } + + @Test + fun `stopProfileSession stops the continuous profiler`() { + val profiler = mock() + val scopes = generateScopes { + it.setContinuousProfiler(profiler) + it.experimental.profileSessionSampleRate = 1.0 + } + scopes.stopProfileSession() + verify(profiler).stopProfileSession(eq(ProfileLifecycle.MANUAL)) + } + + @Test + fun `stopProfileSession logs instructions if continuous profiling is disabled`() { + val profiler = mock() + val logger = mock() + val scopes = generateScopes { + it.setContinuousProfiler(profiler) + it.experimental.profileSessionSampleRate = 1.0 + it.profilesSampleRate = 1.0 + it.setLogger(logger) + it.isDebug = true + } + scopes.stopProfileSession() + verify(profiler, never()).stopProfileSession(eq(ProfileLifecycle.MANUAL)) + verify(logger).log(eq(SentryLevel.WARNING), eq("Continuous Profiling is not enabled. Set profilesSampleRate and profilesSampler to null to enable it.")) + } + + @Test + fun `stopProfileSession is ignored on trace lifecycle`() { + val profiler = mock() + val logger = mock() + val scopes = generateScopes { + it.setContinuousProfiler(profiler) + it.experimental.profileSessionSampleRate = 1.0 + it.experimental.profileLifecycle = ProfileLifecycle.TRACE + it.setLogger(logger) + it.isDebug = true + } + scopes.stopProfileSession() + verify(logger).log(eq(SentryLevel.WARNING), eq("Profiling lifecycle is %s. Profiling cannot be stopped manually."), eq(ProfileLifecycle.TRACE.name)) + verify(profiler, never()).stopProfileSession(any()) + } + + //endregion + @Test fun `null tags do not cause NPE`() { val scopes = generateScopes() diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 63052022ebc..4f50d2eedaa 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -42,6 +42,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 @@ -77,6 +78,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 @@ -96,12 +99,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(), 1.0, 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) @@ -1128,6 +1131,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("")) @@ -1528,6 +1547,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) @@ -3192,6 +3234,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 760d1270e5c..a85e940e22e 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 c2ae7527d8d..e13d3273939 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -193,42 +193,64 @@ 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) + assertFalse(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 false`() { val options = SentryOptions().apply { this.profilesSampleRate = null this.profilesSampler = null } assertFalse(options.isProfilingEnabled) + assertFalse(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 + fun `when profileSessionSampleRate is set to 0, isProfilingEnabled is false and isContinuousProfilingEnabled is false`() { + val options = SentryOptions().apply { + this.experimental.profileSessionSampleRate = 0.0 + } + assertFalse(options.isProfilingEnabled) + assertFalse(options.isContinuousProfilingEnabled) + } + + @Test + fun `when profileSessionSampleRate is null, isProfilingEnabled is false and isContinuousProfilingEnabled is false`() { + val options = SentryOptions() + assertNull(options.experimental.profileSessionSampleRate) + assertFalse(options.isProfilingEnabled) + assertFalse(options.isContinuousProfilingEnabled) } @Test @@ -250,8 +272,54 @@ class SentryOptionsTest { } @Test - fun `when options is initialized, transactionPerformanceCollector is set`() { - assertIs(SentryOptions().transactionPerformanceCollector) + fun `when profileSessionSampleRate is set to exactly 0, value is set`() { + val options = SentryOptions().apply { + this.experimental.profileSessionSampleRate = 0.0 + } + assertEquals(0.0, options.profileSessionSampleRate) + } + + @Test + fun `when profileSessionSampleRate is set to higher than 1_0, setter throws`() { + assertFailsWith { SentryOptions().experimental.profileSessionSampleRate = 1.0000000000001 } + } + + @Test + fun `when profileSessionSampleRate is set to lower than 0, setter throws`() { + assertFailsWith { SentryOptions().experimental.profileSessionSampleRate = -0.0000000000001 } + } + + @Test + fun `when profileLifecycleSessionSampleRate is set to a value, value is set`() { + val options = SentryOptions().apply { + this.experimental.profileLifecycle = ProfileLifecycle.TRACE + } + assertEquals(ProfileLifecycle.TRACE, options.profileLifecycle) + } + + @Test + fun `profileLifecycleSessionSampleRate defaults to MANUAL`() { + val options = SentryOptions() + assertEquals(ProfileLifecycle.MANUAL, options.profileLifecycle) + } + + @Test + fun `when isStartProfilerOnAppStart is set to a value, value is set`() { + val options = SentryOptions().apply { + this.experimental.isStartProfilerOnAppStart = true + } + assertTrue(options.isStartProfilerOnAppStart) + } + + @Test + fun `isStartProfilerOnAppStart defaults to false`() { + val options = SentryOptions() + assertFalse(options.isStartProfilerOnAppStart) + } + + @Test + fun `when options is initialized, compositePerformanceCollector is set`() { + assertIs(SentryOptions().compositePerformanceCollector) } @Test @@ -259,6 +327,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()) @@ -470,16 +543,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 @@ -570,10 +643,18 @@ class SentryOptionsTest { fun `when profiling is disabled, isEnableAppStartProfiling is always false`() { val options = SentryOptions() options.isEnableAppStartProfiling = true - options.profilesSampleRate = 0.0 + options.experimental.profileSessionSampleRate = 0.0 assertFalse(options.isEnableAppStartProfiling) } + @Test + fun `when setEnableAppStartProfiling is called and continuous profiling is enabled, isEnableAppStartProfiling is true`() { + val options = SentryOptions() + options.isEnableAppStartProfiling = true + options.experimental.profileSessionSampleRate = 1.0 + assertTrue(options.isEnableAppStartProfiling) + } + @Test fun `when options are initialized, profilingTracesHz is set to 101 by default`() { assertEquals(101, SentryOptions().profilingTracesHz) diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index 28c2fe3367b..e4a260a2a3b 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -401,6 +401,35 @@ class SentryTest { assertTrue(File(sentryOptions?.profilingTracesDirPath!!).list()!!.isEmpty()) } + @Test + fun `profilingTracesDirPath should be created and cleared at initialization when continuous profiling is enabled`() { + val tempPath = getTempPath() + var sentryOptions: SentryOptions? = null + Sentry.init { + it.dsn = dsn + it.experimental.profileSessionSampleRate = 1.0 + it.cacheDirPath = tempPath + sentryOptions = it + } + + assertTrue(File(sentryOptions?.profilingTracesDirPath!!).exists()) + assertTrue(File(sentryOptions?.profilingTracesDirPath!!).list()!!.isEmpty()) + } + + @Test + fun `profilingTracesDirPath should not be created when no profiling is enabled`() { + val tempPath = getTempPath() + var sentryOptions: SentryOptions? = null + Sentry.init { + it.dsn = dsn + it.experimental.profileSessionSampleRate = 0.0 + it.cacheDirPath = tempPath + sentryOptions = it + } + + assertFalse(File(sentryOptions?.profilingTracesDirPath!!).exists()) + } + @Test fun `only old profiles in profilingTracesDirPath should be cleared when profiling is enabled`() { val tempPath = getTempPath() @@ -1102,6 +1131,25 @@ class SentryTest { ) } + @Test + fun `init calls samplers if isStartProfilerOnAppStart is true`() { + val mockSampleTracer = mock() + val mockProfilesSampler = mock() + Sentry.init { + it.dsn = dsn + it.tracesSampleRate = 1.0 + it.experimental.isStartProfilerOnAppStart = true + it.profilesSampleRate = 1.0 + it.tracesSampler = mockSampleTracer + it.profilesSampler = mockProfilesSampler + it.executorService = ImmediateExecutorService() + it.cacheDirPath = getTempPath() + } + // Samplers are not called + verify(mockSampleTracer, never()).sample(any()) + verify(mockProfilesSampler, never()).sample(any()) + } + @Test fun `init calls app start profiling samplers in the background`() { val mockSampleTracer = mock() @@ -1192,6 +1240,24 @@ class SentryTest { assertTrue(appStartProfilingConfigFile.exists()) } + @Test + fun `init creates app start profiling config if isStartProfilerOnAppStart, even with performance disabled`() { + val path = getTempPath() + File(path).mkdirs() + val appStartProfilingConfigFile = File(path, "app_start_profiling_config") + appStartProfilingConfigFile.createNewFile() + assertTrue(appStartProfilingConfigFile.exists()) + Sentry.init { + it.dsn = dsn + it.cacheDirPath = path + it.isEnableAppStartProfiling = false + it.experimental.isStartProfilerOnAppStart = true + it.tracesSampleRate = 0.0 + it.executorService = ImmediateExecutorService() + } + assertTrue(appStartProfilingConfigFile.exists()) + } + @Test fun `init saves SentryAppStartProfilingOptions to disk`() { var options = SentryOptions() @@ -1199,9 +1265,9 @@ class SentryTest { initForTest { it.dsn = dsn it.cacheDirPath = path - it.tracesSampleRate = 1.0 it.tracesSampleRate = 0.5 it.isEnableAppStartProfiling = true + it.experimental.isStartProfilerOnAppStart = true it.profilesSampleRate = 0.2 it.executorService = ImmediateExecutorService() options = it @@ -1214,6 +1280,8 @@ class SentryTest { assertEquals(0.5, appStartOption.traceSampleRate) assertEquals(0.2, appStartOption.profileSampleRate) assertTrue(appStartOption.isProfilingEnabled) + assertTrue(appStartOption.isEnableAppStartProfiling) + assertTrue(appStartOption.isStartProfilerOnAppStart) } @Test @@ -1278,6 +1346,75 @@ class SentryTest { assertNotSame(s1, s2) } + @Test + fun `startProfileSession starts the continuous profiler`() { + val profiler = mock() + Sentry.init { + it.dsn = dsn + it.setContinuousProfiler(profiler) + it.experimental.profileSessionSampleRate = 1.0 + } + Sentry.startProfileSession() + verify(profiler).startProfileSession(eq(ProfileLifecycle.MANUAL), any()) + } + + @Test + fun `startProfileSession is ignored when continuous profiling is disabled`() { + val profiler = mock() + Sentry.init { + it.dsn = dsn + it.setContinuousProfiler(profiler) + it.profilesSampleRate = 1.0 + } + Sentry.startProfileSession() + verify(profiler, never()).startProfileSession(eq(ProfileLifecycle.MANUAL), any()) + } + + @Test + fun `startProfileSession is ignored when profile lifecycle is TRACE`() { + val profiler = mock() + val logger = mock() + Sentry.init { + it.dsn = dsn + it.setContinuousProfiler(profiler) + it.experimental.profileSessionSampleRate = 1.0 + it.experimental.profileLifecycle = ProfileLifecycle.TRACE + it.isDebug = true + it.setLogger(logger) + } + Sentry.startProfileSession() + verify(profiler, never()).startProfileSession(any(), any()) + verify(logger).log( + eq(SentryLevel.WARNING), + eq("Profiling lifecycle is %s. Profiling cannot be started manually."), + eq(ProfileLifecycle.TRACE.name) + ) + } + + @Test + fun `stopProfileSession stops the continuous profiler`() { + val profiler = mock() + Sentry.init { + it.dsn = dsn + it.setContinuousProfiler(profiler) + it.experimental.profileSessionSampleRate = 1.0 + } + Sentry.stopProfileSession() + verify(profiler).stopProfileSession(eq(ProfileLifecycle.MANUAL)) + } + + @Test + fun `stopProfileSession is ignored when continuous profiling is disabled`() { + val profiler = mock() + Sentry.init { + it.dsn = dsn + it.setContinuousProfiler(profiler) + it.profilesSampleRate = 1.0 + } + Sentry.stopProfileSession() + verify(profiler, never()).stopProfileSession(eq(ProfileLifecycle.MANUAL)) + } + private class InMemoryOptionsObserver : IOptionsObserver { var release: String? = null private set @@ -1329,6 +1466,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 8c8bd323c82..f5ae2c4f004 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,114 @@ 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) + }, samplingDecision = TracesSamplingDecision(true)) + tracer.finish() + verify(fixture.scopes).captureTransaction( + check { + assertNotNull(it.contexts.profile) { + assertEquals(profilerId, it.profilerId) + } + }, + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + + @Test + fun `when transaction is not sampled, profile context is not set`() { + val continuousProfiler = mock() + val profilerId = SentryId() + whenever(continuousProfiler.profilerId).thenReturn(profilerId) + val tracer = fixture.getSut(optionsConfiguration = { + it.setContinuousProfiler(continuousProfiler) + }, samplingDecision = TracesSamplingDecision(false)) + tracer.finish() + // profiler is never stopped, as it was never started + verify(continuousProfiler, never()).stopProfileSession(any()) + // profile context is not set + verify(fixture.scopes).captureTransaction( + check { + assertNull(it.contexts.profile) + }, + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + } + + @Test + fun `when continuous profiler is running in MANUAL mode, profiler is not stopped on transaction finish`() { + val continuousProfiler = mock() + val profilerId = SentryId() + whenever(continuousProfiler.profilerId).thenReturn(profilerId) + val tracer = fixture.getSut(optionsConfiguration = { + it.setContinuousProfiler(continuousProfiler) + it.experimental.profileLifecycle = ProfileLifecycle.MANUAL + }, samplingDecision = TracesSamplingDecision(true)) + tracer.finish() + // profiler is never stopped, as it should be stopped manually + verify(continuousProfiler, never()).stopProfileSession(any()) + } + + @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) + }, samplingDecision = TracesSamplingDecision(true)) + val span = tracer.startChild("span.op") + assertEquals(profilerId.toString(), span.getData(SpanDataConvention.PROFILER_ID)) + } + + @Test + fun `when transaction is not sampled, profiler id is NOT set in span data`() { + val profilerId = SentryId() + val profiler = mock() + whenever(profiler.profilerId).thenReturn(profilerId) + + val tracer = fixture.getSut(optionsConfiguration = { options -> + options.setContinuousProfiler(profiler) + }, samplingDecision = TracesSamplingDecision(false)) + val span = tracer.startChild("span.op") + assertNull(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 +1138,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 +1320,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 +1477,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 +1491,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 bbbb72a0f05..31c07b16de6 100644 --- a/sentry/src/test/java/io/sentry/SpanContextTest.kt +++ b/sentry/src/test/java/io/sentry/SpanContextTest.kt @@ -2,6 +2,7 @@ package io.sentry import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -14,6 +15,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") @@ -47,6 +55,6 @@ class SpanContextTest { trace.setData("k", "v") trace.setData("k", null) trace.setData(null, null) - assertTrue(trace.data.isEmpty()) + assertFalse(trace.data.containsKey("k")) } } diff --git a/sentry/src/test/java/io/sentry/TracesSamplerTest.kt b/sentry/src/test/java/io/sentry/TracesSamplerTest.kt index ff4bc6dc1da..99718735a1e 100644 --- a/sentry/src/test/java/io/sentry/TracesSamplerTest.kt +++ b/sentry/src/test/java/io/sentry/TracesSamplerTest.kt @@ -1,5 +1,6 @@ package io.sentry +import io.sentry.util.SentryRandom import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.mock @@ -16,6 +17,7 @@ class TracesSamplerTest { internal fun getSut( tracesSampleRate: Double? = null, profilesSampleRate: Double? = null, + profileSessionSampleRate: Double? = null, tracesSamplerCallback: SentryOptions.TracesSamplerCallback? = null, profilesSamplerCallback: SentryOptions.ProfilesSamplerCallback? = null, logger: ILogger? = null @@ -27,6 +29,9 @@ class TracesSamplerTest { if (profilesSampleRate != null) { options.profilesSampleRate = profilesSampleRate } + if (profileSessionSampleRate != null) { + options.experimental.profileSessionSampleRate = profileSessionSampleRate + } if (tracesSamplerCallback != null) { options.tracesSampler = tracesSamplerCallback } @@ -160,6 +165,28 @@ class TracesSamplerTest { assertEquals(0.9, samplingDecision.sampleRand) } + @Test + fun `when profileSessionSampleRate is not set returns false`() { + val sampler = fixture.getSut() + val sampled = sampler.sampleSessionProfile(1.0) + assertFalse(sampled) + } + + @Test + fun `when profileSessionSampleRate is set and random returns lower number returns true`() { + SentryRandom.current().nextDouble() + val sampler = fixture.getSut(profileSessionSampleRate = 0.2) + val sampled = sampler.sampleSessionProfile(0.1) + assertTrue(sampled) + } + + @Test + fun `when profileSessionSampleRate is set and random returns greater number returns false`() { + val sampler = fixture.getSut(profileSessionSampleRate = 0.2) + val sampled = sampler.sampleSessionProfile(0.9) + assertFalse(sampled) + } + @Test fun `when tracesSampler returns null and parentSampled is set sampler uses it as a sampling decision`() { val sampler = fixture.getSut(tracesSamplerCallback = null) diff --git a/sentry/src/test/java/io/sentry/protocol/ContextsTest.kt b/sentry/src/test/java/io/sentry/protocol/ContextsTest.kt index e1ffe73c0cd..e06ac18332c 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 @@ -20,6 +21,7 @@ class ContextsTest { contexts.setGpu(Gpu()) contexts.setResponse(Response()) contexts.setTrace(SpanContext("op")) + contexts.profile = ProfileContext(SentryId()) contexts.setSpring(Spring()) val clone = Contexts(contexts) @@ -33,6 +35,7 @@ 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) assertNotSame(contexts.spring, clone.spring) } @@ -40,9 +43,11 @@ class ContextsTest { @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) @@ -50,6 +55,7 @@ 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) } @Test diff --git a/sentry/src/test/java/io/sentry/protocol/DebugMetaTest.kt b/sentry/src/test/java/io/sentry/protocol/DebugMetaTest.kt index 17544a300ff..21395dfc5c0 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 707daa78f10..2ebc830a5ef 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/transport/RateLimiterTest.kt b/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt index ade4ab88b93..5be5a22802c 100644 --- a/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt +++ b/sentry/src/test/java/io/sentry/transport/RateLimiterTest.kt @@ -9,6 +9,7 @@ import io.sentry.ILogger import io.sentry.IScopes import io.sentry.ISerializer import io.sentry.NoOpLogger +import io.sentry.ProfileChunk import io.sentry.ProfilingTraceData import io.sentry.ReplayRecording import io.sentry.SentryEnvelope @@ -210,8 +211,9 @@ class RateLimiterTest { val attachmentItem = SentryEnvelopeItem.fromAttachment(fixture.serializer, NoOpLogger.getInstance(), Attachment("{ \"number\": 10 }".toByteArray(), "log.json"), 1000) val profileItem = SentryEnvelopeItem.fromProfilingTrace(ProfilingTraceData(File(""), transaction), 1000, fixture.serializer) val checkInItem = SentryEnvelopeItem.fromCheckIn(fixture.serializer, CheckIn("monitor-slug-1", CheckInStatus.ERROR)) + val profileChunkItem = SentryEnvelopeItem.fromProfileChunk(ProfileChunk(), fixture.serializer) - val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem, userFeedbackItem, sessionItem, attachmentItem, profileItem, checkInItem)) + val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(eventItem, userFeedbackItem, sessionItem, attachmentItem, profileItem, checkInItem, profileChunkItem)) rateLimiter.updateRetryAfterLimits(null, null, 429) val result = rateLimiter.filter(envelope, Hint()) @@ -224,6 +226,7 @@ class RateLimiterTest { verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(attachmentItem)) verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(profileItem)) verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(checkInItem)) + verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(profileChunkItem)) verifyNoMoreInteractions(fixture.clientReportRecorder) } @@ -333,6 +336,24 @@ class RateLimiterTest { verifyNoMoreInteractions(fixture.clientReportRecorder) } + @Test + fun `drop profileChunk items as lost`() { + val rateLimiter = fixture.getSUT() + + val profileChunkItem = SentryEnvelopeItem.fromProfileChunk(ProfileChunk(), fixture.serializer) + val attachmentItem = SentryEnvelopeItem.fromAttachment(fixture.serializer, NoOpLogger.getInstance(), Attachment("{ \"number\": 10 }".toByteArray(), "log.json"), 1000) + val envelope = SentryEnvelope(SentryEnvelopeHeader(), arrayListOf(profileChunkItem, attachmentItem)) + + rateLimiter.updateRetryAfterLimits("60:profile_chunk:key", null, 1) + val result = rateLimiter.filter(envelope, Hint()) + + assertNotNull(result) + assertEquals(1, result.items.toList().size) + + verify(fixture.clientReportRecorder, times(1)).recordLostEnvelopeItem(eq(DiscardReason.RATELIMIT_BACKOFF), same(profileChunkItem)) + verifyNoMoreInteractions(fixture.clientReportRecorder) + } + @Test fun `apply rate limits notifies observers`() { val rateLimiter = fixture.getSUT() diff --git a/sentry/src/test/java/io/sentry/util/SampleRateUtilTest.kt b/sentry/src/test/java/io/sentry/util/SampleRateUtilTest.kt index 8576e9b10f2..ef099d7d501 100644 --- a/sentry/src/test/java/io/sentry/util/SampleRateUtilTest.kt +++ b/sentry/src/test/java/io/sentry/util/SampleRateUtilTest.kt @@ -134,6 +134,46 @@ class SampleRateUtilTest { assertTrue(SampleRateUtils.isValidProfilesSampleRate(null)) } + @Test + fun `accepts 0 for continuous profiles sample rate`() { + assertTrue(SampleRateUtils.isValidContinuousProfilesSampleRate(0.0)) + } + + @Test + fun `accepts 1 for continuous profiles sample rate`() { + assertTrue(SampleRateUtils.isValidContinuousProfilesSampleRate(1.0)) + } + + @Test + fun `accepts null continuous profiles sample rate`() { + assertTrue(SampleRateUtils.isValidProfilesSampleRate(null)) + } + + @Test + fun `rejects negative continuous profiles sample rate`() { + assertFalse(SampleRateUtils.isValidContinuousProfilesSampleRate(-0.5)) + } + + @Test + fun `rejects 1 dot 01 for continuous profiles sample rate`() { + assertFalse(SampleRateUtils.isValidContinuousProfilesSampleRate(1.01)) + } + + @Test + fun `rejects NaN continuous profiles sample rate`() { + assertFalse(SampleRateUtils.isValidContinuousProfilesSampleRate(Double.NaN)) + } + + @Test + fun `rejects positive infinite continuous profiles sample rate`() { + assertFalse(SampleRateUtils.isValidContinuousProfilesSampleRate(Double.POSITIVE_INFINITY)) + } + + @Test + fun `rejects negative infinite continuous profiles sample rate`() { + assertFalse(SampleRateUtils.isValidContinuousProfilesSampleRate(Double.NEGATIVE_INFINITY)) + } + @Test fun `fills sample rand on decision if missing`() { val decision = SampleRateUtils.backfilledSampleRand(TracesSamplingDecision(true)) 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 26de021fbdc..12b1e348271 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 8c396858784..c2bff2a050e 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 8281ca67abb..395bb03bbab 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 c1e97cd9dd3..8eb4000fc65 100644 --- a/sentry/src/test/resources/json/contexts.json +++ b/sentry/src/test/resources/json/contexts.json @@ -127,7 +127,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 d2d1fd00881..63ae8f03cf3 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 4ce74eaf09e..2079b424cb6 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 e250b1c95d2..1a79632582e 100644 --- a/sentry/src/test/resources/json/sentry_event.json +++ b/sentry/src/test/resources/json/sentry_event.json @@ -265,7 +265,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 d3970bf5b00..7bd64037d77 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 33080c9686e..daa6d025e9a 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 0d6ed5eb095..316b44bbaaa 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 2d965be1b9c..cf927b322b6 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 c55841a391b..edff574fa4d 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 } }