From 4f4a194d6778c5a16f6c3708924d22b6d9a444f7 Mon Sep 17 00:00:00 2001 From: Stefano Date: Mon, 14 Mar 2022 18:03:22 +0100 Subject: [PATCH] Feature: Android profiling traces (#1897) * Added a transaction profiler (noOp on java sdk) that starts/stops android profiling when a transaction starts or is finished * Added a new envelope item type "profile", generated using android trace file. Its payload is a json represented by ProfilingTraceData * SentryExecutorService now uses a ScheduledExecutorService to allow the schedule() method * Added "profilingTracesDirPath" and "profilingTracesIntervalMillis" to SentryAndroidOptions * Added "profilingEnabled", "maxTraceFileSize" and "transactionProfiler" to SentryOptions * Added io.sentry.traces.profiling.enable to manifest options * Profiling is disabled by default * Profiling traces directory's content is deleted in the background when the options are initialized --- CHANGELOG.md | 2 + .../api/sentry-android-core.api | 14 +- .../core/AndroidOptionsInitializer.java | 28 +- .../core/AndroidTransactionProfiler.java | 256 ++++++++++++++++++ .../android/core/BuildInfoProvider.java | 56 ++++ .../io/sentry/android/core/ContextUtils.java | 11 + .../core/DefaultAndroidEventProcessor.java | 33 +-- .../android/core/IBuildInfoProvider.java | 32 +++ .../android/core/ManifestMetadataReader.java | 5 + .../android/core/SentryAndroidOptions.java | 43 +++ .../core/internal/util/CpuInfoUtils.java | 54 ++++ .../core/ActivityLifecycleIntegrationTest.kt | 16 +- .../core/ManifestMetadataReaderTest.kt | 25 ++ .../apollo/SentryApolloInterceptorTest.kt | 10 + .../spring/tracing/SentryTracingFilterTest.kt | 12 + .../tracing/SentryTransactionAdviceTest.kt | 10 + sentry/api/sentry.api | 74 ++++- sentry/src/main/java/io/sentry/Hub.java | 16 +- .../src/main/java/io/sentry/HubAdapter.java | 6 +- sentry/src/main/java/io/sentry/IHub.java | 21 +- .../main/java/io/sentry/ISentryClient.java | 27 +- .../io/sentry/ISentryExecutorService.java | 7 +- .../java/io/sentry/ITransactionProfiler.java | 14 + sentry/src/main/java/io/sentry/NoOpHub.java | 3 +- .../main/java/io/sentry/NoOpSentryClient.java | 3 +- .../io/sentry/NoOpSentryExecutorService.java | 5 + .../io/sentry/NoOpTransactionProfiler.java | 23 ++ .../java/io/sentry/ProfilingTraceData.java | 192 +++++++++++++ .../src/main/java/io/sentry/SentryClient.java | 29 +- .../java/io/sentry/SentryEnvelopeItem.java | 139 +++++++--- .../java/io/sentry/SentryExecutorService.java | 13 +- .../main/java/io/sentry/SentryItemType.java | 1 + .../main/java/io/sentry/SentryOptions.java | 70 ++++- .../src/main/java/io/sentry/SentryTracer.java | 6 +- .../sentry/transport/ReusableCountLatch.java | 8 +- .../main/java/io/sentry/util/FileUtils.java | 59 ++++ sentry/src/test/java/io/sentry/HubTest.kt | 4 +- .../test/java/io/sentry/OutboxSenderTest.kt | 1 + .../java/io/sentry/SentryEnvelopeItemTest.kt | 4 +- .../io/sentry/SentryExecutorServiceTest.kt | 14 +- .../test/java/io/sentry/SentryTracerTest.kt | 27 +- 41 files changed, 1238 insertions(+), 135 deletions(-) create mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java create mode 100644 sentry-android-core/src/main/java/io/sentry/android/core/internal/util/CpuInfoUtils.java create mode 100644 sentry/src/main/java/io/sentry/ITransactionProfiler.java create mode 100644 sentry/src/main/java/io/sentry/NoOpTransactionProfiler.java create mode 100644 sentry/src/main/java/io/sentry/ProfilingTraceData.java create mode 100644 sentry/src/main/java/io/sentry/util/FileUtils.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e43c54709..dc185310da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +* Feat: Add Android profiling traces #1897 + ## 5.6.2 ### Various fixes & improvements diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api index 689c85e35a..5799c87a35 100644 --- a/sentry-android-core/api/sentry-android-core.api +++ b/sentry-android-core/api/sentry-android-core.api @@ -58,9 +58,13 @@ public final class io/sentry/android/core/BuildConfig { } public final class io/sentry/android/core/BuildInfoProvider : io/sentry/android/core/IBuildInfoProvider { - public fun ()V + public fun (Lio/sentry/ILogger;)V public fun getBuildTags ()Ljava/lang/String; + public fun getManufacturer ()Ljava/lang/String; + public fun getModel ()Ljava/lang/String; public fun getSdkInfoVersion ()I + public fun getVersionRelease ()Ljava/lang/String; + public fun isEmulator ()Ljava/lang/Boolean; } public abstract class io/sentry/android/core/EnvelopeFileObserverIntegration : io/sentry/Integration, java/io/Closeable { @@ -72,7 +76,11 @@ public abstract class io/sentry/android/core/EnvelopeFileObserverIntegration : i public abstract interface class io/sentry/android/core/IBuildInfoProvider { public abstract fun getBuildTags ()Ljava/lang/String; + public abstract fun getManufacturer ()Ljava/lang/String; + public abstract fun getModel ()Ljava/lang/String; public abstract fun getSdkInfoVersion ()I + public abstract fun getVersionRelease ()Ljava/lang/String; + public abstract fun isEmulator ()Ljava/lang/Boolean; } public abstract interface class io/sentry/android/core/IDebugImagesLoader { @@ -110,6 +118,8 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun enableAllAutoBreadcrumbs (Z)V public fun getAnrTimeoutIntervalMillis ()J public fun getDebugImagesLoader ()Lio/sentry/android/core/IDebugImagesLoader; + public fun getProfilingTracesDirPath ()Ljava/lang/String; + public fun getProfilingTracesIntervalMillis ()I public fun isAnrEnabled ()Z public fun isAnrReportInDebug ()Z public fun isEnableActivityLifecycleBreadcrumbs ()Z @@ -130,6 +140,8 @@ public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/Sentr public fun setEnableAutoActivityLifecycleTracing (Z)V public fun setEnableSystemEventBreadcrumbs (Z)V public fun setEnableUserInteractionBreadcrumbs (Z)V + public fun setProfilingTracesDirPath (Ljava/lang/String;)V + public fun setProfilingTracesIntervalMillis (I)V } public final class io/sentry/android/core/SentryInitProvider : android/content/ContentProvider { 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 d69c8890c2..3a6b3637af 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 @@ -13,6 +13,7 @@ import io.sentry.SendFireAndForgetOutboxSender; import io.sentry.SentryLevel; import io.sentry.SentryOptions; +import io.sentry.util.FileUtils; import io.sentry.util.Objects; import java.io.BufferedInputStream; import java.io.File; @@ -57,7 +58,7 @@ static void init( final @NotNull SentryAndroidOptions options, @NotNull Context context, final @NotNull ILogger logger) { - init(options, context, logger, new BuildInfoProvider()); + init(options, context, logger, new BuildInfoProvider(logger)); } /** @@ -117,6 +118,8 @@ static void init( options.addEventProcessor(new PerformanceAndroidEventProcessor(options, activityFramesTracker)); options.setTransportGate(new AndroidTransportGate(context, options.getLogger())); + options.setTransactionProfiler( + new AndroidTransactionProfiler(context, options, buildInfoProvider)); } private static void installDefaultIntegrations( @@ -248,10 +251,31 @@ private static void readDefaultOptionValues( * @param context the Application context * @param options the SentryOptions */ + @SuppressWarnings("FutureReturnValueIgnored") private static void initializeCacheDirs( - final @NotNull Context context, final @NotNull SentryOptions options) { + final @NotNull Context context, final @NotNull SentryAndroidOptions options) { final File cacheDir = new File(context.getCacheDir(), "sentry"); + final File profilingTracesDir = new File(cacheDir, "profiling_traces"); options.setCacheDirPath(cacheDir.getAbsolutePath()); + options.setProfilingTracesDirPath(profilingTracesDir.getAbsolutePath()); + + if (options.isProfilingEnabled()) { + profilingTracesDir.mkdirs(); + final File[] oldTracesDirContent = profilingTracesDir.listFiles(); + + options + .getExecutorService() + .submit( + () -> { + if (oldTracesDirContent == null) return; + // Method trace files are normally deleted at the end of traces, but if that fails + // for some + // reason we try to clear any old files here. + for (File f : oldTracesDirContent) { + FileUtils.deleteRecursively(f); + } + }); + } } private static boolean isNdkAvailable(final @NotNull IBuildInfoProvider buildInfoProvider) { 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 new file mode 100644 index 0000000000..bcb77554ec --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java @@ -0,0 +1,256 @@ +package io.sentry.android.core; + +import static android.content.Context.ACTIVITY_SERVICE; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import android.annotation.SuppressLint; +import android.app.ActivityManager; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.os.Build; +import android.os.Debug; +import android.os.SystemClock; +import io.sentry.ITransaction; +import io.sentry.ITransactionProfiler; +import io.sentry.ProfilingTraceData; +import io.sentry.SentryLevel; +import io.sentry.android.core.internal.util.CpuInfoUtils; +import io.sentry.util.Objects; +import java.io.File; +import java.util.UUID; +import java.util.concurrent.Future; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +final class AndroidTransactionProfiler implements ITransactionProfiler { + + /** + * This appears to correspond to the buffer size of the data part of the file, excluding the key + * part. Once the buffer is full, new records are ignored, but the resulting trace file will be + * valid. + * + *

30 second traces can require a buffer of a few MB. 8MB is the default buffer size for + * [Debug.startMethodTracingSampling], but 3 should be enough for most cases. We can adjust this + * in the future if we notice that traces are being truncated in some applications. + */ + private static final int BUFFER_SIZE_BYTES = 3_000_000; + + private static final int PROFILING_TIMEOUT_MILLIS = 30_000; + + private int intervalUs; + private @Nullable File traceFile = null; + private @Nullable File traceFilesDir = null; + private @Nullable Future scheduledFinish = null; + private volatile @Nullable ITransaction activeTransaction = null; + private volatile @Nullable ProfilingTraceData timedOutProfilingData = null; + private final @NotNull Context context; + private final @NotNull SentryAndroidOptions options; + private final @NotNull IBuildInfoProvider buildInfoProvider; + private final @Nullable PackageInfo packageInfo; + private long transactionStartNanos = 0; + + public AndroidTransactionProfiler( + final @NotNull Context context, + final @NotNull SentryAndroidOptions sentryAndroidOptions, + final @NotNull IBuildInfoProvider buildInfoProvider) { + this.context = Objects.requireNonNull(context, "The application context is required"); + this.options = Objects.requireNonNull(sentryAndroidOptions, "SentryAndroidOptions is required"); + this.buildInfoProvider = + Objects.requireNonNull(buildInfoProvider, "The BuildInfoProvider is required."); + this.packageInfo = ContextUtils.getPackageInfo(context, options.getLogger()); + final String tracesFilesDirPath = options.getProfilingTracesDirPath(); + if (tracesFilesDirPath == null || tracesFilesDirPath.isEmpty()) { + options + .getLogger() + .log( + SentryLevel.WARNING, + "Disabling profiling because no profiling traces dir path is defined in options."); + return; + } + long intervalMillis = options.getProfilingTracesIntervalMillis(); + if (intervalMillis <= 0) { + options + .getLogger() + .log( + SentryLevel.WARNING, + "Disabling profiling because trace interval is set to %d milliseconds", + intervalMillis); + return; + } + intervalUs = (int) MILLISECONDS.toMicros(intervalMillis); + traceFilesDir = new File(tracesFilesDirPath); + } + + @SuppressLint("NewApi") + @Override + public synchronized void onTransactionStart(@NotNull ITransaction transaction) { + + // Debug.startMethodTracingSampling() is only available since Lollipop + if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP) return; + + // traceFilesDir is null or intervalUs is 0 only if there was a problem in the constructor, but + // we already logged that + if (traceFilesDir == null || intervalUs == 0) { + return; + } + + // If a transaction is currently being profiled, we ignore this call + if (activeTransaction != null) { + options + .getLogger() + .log( + SentryLevel.WARNING, + "Profiling is already active and was started by transaction %s", + activeTransaction.getSpanContext().getTraceId().toString()); + return; + } + + traceFile = new File(traceFilesDir, UUID.randomUUID() + ".trace"); + + if (traceFile.exists()) { + options + .getLogger() + .log(SentryLevel.DEBUG, "Trace file already exists: %s", traceFile.getPath()); + return; + } + activeTransaction = transaction; + + // We stop the trace after 30 seconds, since such a long trace is very probably a trace + // that will never end due to an error + scheduledFinish = + options + .getExecutorService() + .schedule( + () -> timedOutProfilingData = onTransactionFinish(transaction), + PROFILING_TIMEOUT_MILLIS); + + transactionStartNanos = SystemClock.elapsedRealtimeNanos(); + Debug.startMethodTracingSampling(traceFile.getPath(), BUFFER_SIZE_BYTES, intervalUs); + } + + @SuppressLint("NewApi") + @Override + public synchronized @Nullable ProfilingTraceData onTransactionFinish( + @NotNull ITransaction transaction) { + + // onTransactionStart() is only available since Lollipop + // and SystemClock.elapsedRealtimeNanos() since Jelly Bean + if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP) return null; + + final ITransaction finalActiveTransaction = activeTransaction; + final ProfilingTraceData profilingData = timedOutProfilingData; + + // Profiling finished, but we check if we cached last profiling data due to a timeout + if (finalActiveTransaction == null) { + // If the cached timed out profiling data refers to the transaction that started it we return + // it back, otherwise we would simply lose it + if (profilingData != null) { + // The timed out transaction is finishing + if (profilingData + .getTraceId() + .equals(transaction.getSpanContext().getTraceId().toString())) { + timedOutProfilingData = null; + return profilingData; + } else { + // Another transaction is finishing before the timed out one + options + .getLogger() + .log( + SentryLevel.ERROR, + "Profiling data with id %s exists but doesn't match the closing transaction %s", + profilingData.getTraceId(), + transaction.getSpanContext().getTraceId().toString()); + return null; + } + } + // A transaction is finishing, but profiling didn't start. Maybe it was started by another one + options + .getLogger() + .log( + SentryLevel.INFO, + "Transaction %s finished, but profiling never started for it. Skipping", + transaction.getSpanContext().getTraceId().toString()); + return null; + } + + if (finalActiveTransaction != transaction) { + options + .getLogger() + .log( + SentryLevel.DEBUG, + "Transaction %s finished, but profiling was started by transaction %s. Skipping", + transaction.getSpanContext().getTraceId().toString(), + finalActiveTransaction.getSpanContext().getTraceId().toString()); + return null; + } + + Debug.stopMethodTracing(); + long transactionDurationNanos = SystemClock.elapsedRealtimeNanos() - transactionStartNanos; + + activeTransaction = null; + + if (scheduledFinish != null) { + scheduledFinish.cancel(true); + scheduledFinish = null; + } + + if (traceFile == null || !traceFile.exists()) { + options + .getLogger() + .log( + SentryLevel.ERROR, + "Trace file %s does not exists", + traceFile == null ? "null" : traceFile.getPath()); + return null; + } + + String versionName = ""; + String versionCode = ""; + String totalMem = "0"; + ActivityManager.MemoryInfo memInfo = getMemInfo(); + if (packageInfo != null) { + versionName = ContextUtils.getVersionName(packageInfo); + versionCode = ContextUtils.getVersionCode(packageInfo); + } + if (memInfo != null) { + totalMem = Long.toString(memInfo.totalMem); + } + + return new ProfilingTraceData( + traceFile, + transaction, + Long.toString(transactionDurationNanos), + buildInfoProvider.getSdkInfoVersion(), + buildInfoProvider.getManufacturer(), + buildInfoProvider.getModel(), + buildInfoProvider.getVersionRelease(), + buildInfoProvider.isEmulator(), + CpuInfoUtils.readMaxFrequencies(), + totalMem, + options.getProguardUuid(), + versionName, + versionCode, + options.getEnvironment()); + } + + /** + * Get MemoryInfo object representing the memory state of the application. + * + * @return MemoryInfo object representing the memory state of the application + */ + private @Nullable ActivityManager.MemoryInfo getMemInfo() { + try { + ActivityManager actManager = (ActivityManager) context.getSystemService(ACTIVITY_SERVICE); + ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo(); + if (actManager != null) { + actManager.getMemoryInfo(memInfo); + return memInfo; + } + options.getLogger().log(SentryLevel.INFO, "Error getting MemoryInfo."); + return null; + } catch (Throwable e) { + options.getLogger().log(SentryLevel.ERROR, "Error getting MemoryInfo.", e); + return null; + } + } +} diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/BuildInfoProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/BuildInfoProvider.java index 2b8fdc811f..4ed441db99 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/BuildInfoProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/BuildInfoProvider.java @@ -1,13 +1,22 @@ package io.sentry.android.core; import android.os.Build; +import io.sentry.ILogger; +import io.sentry.SentryLevel; +import io.sentry.util.Objects; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; /** The Android Impl. of IBuildInfoProvider which returns the Build class info. */ @ApiStatus.Internal public final class BuildInfoProvider implements IBuildInfoProvider { + final @NotNull ILogger logger; + + public BuildInfoProvider(final @NotNull ILogger logger) { + this.logger = Objects.requireNonNull(logger, "The ILogger object is required."); + } /** * Returns the Build.VERSION.SDK_INT * @@ -22,4 +31,51 @@ public int getSdkInfoVersion() { public @Nullable String getBuildTags() { return Build.TAGS; } + + @Override + public @Nullable String getManufacturer() { + return Build.MANUFACTURER; + } + + @Override + public @Nullable String getModel() { + return Build.MODEL; + } + + @Override + public @Nullable String getVersionRelease() { + return Build.VERSION.RELEASE; + } + + /** + * Check whether the application is running in an emulator. + * https://github.com/flutter/plugins/blob/master/packages/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/DeviceInfoPlugin.java#L105 + * + * @return true if the application is running in an emulator, false otherwise + */ + @Override + public @Nullable Boolean isEmulator() { + try { + return (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) + || Build.FINGERPRINT.startsWith("generic") + || Build.FINGERPRINT.startsWith("unknown") + || Build.HARDWARE.contains("goldfish") + || Build.HARDWARE.contains("ranchu") + || Build.MODEL.contains("google_sdk") + || Build.MODEL.contains("Emulator") + || Build.MODEL.contains("Android SDK built for x86") + || Build.MANUFACTURER.contains("Genymotion") + || Build.PRODUCT.contains("sdk_google") + || Build.PRODUCT.contains("google_sdk") + || Build.PRODUCT.contains("sdk") + || Build.PRODUCT.contains("sdk_x86") + || Build.PRODUCT.contains("vbox86p") + || Build.PRODUCT.contains("emulator") + || Build.PRODUCT.contains("simulator"); + } catch (Throwable e) { + logger.log( + SentryLevel.ERROR, "Error checking whether application is running in an emulator.", e); + return null; + } + } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java b/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java index e577576f25..7e8b0c18b8 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ContextUtils.java @@ -41,6 +41,17 @@ static String getVersionCode(final @NotNull PackageInfo packageInfo) { return getVersionCodeDep(packageInfo); } + /** + * Returns the App's version name based on the PackageInfo + * + * @param packageInfo the PackageInfo class + * @return the versionName + */ + @Nullable + static String getVersionName(final @NotNull PackageInfo packageInfo) { + return packageInfo.versionName; + } + @SuppressWarnings("deprecation") private static @NotNull String getVersionCodeDep(final @NotNull PackageInfo packageInfo) { return Integer.toString(packageInfo.versionCode); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java index 3b5539536b..df1b6ef00d 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java @@ -105,7 +105,7 @@ public DefaultAndroidEventProcessor( } // its not IO, but it has been cached in the old version as well - map.put(EMULATOR, isEmulator()); + map.put(EMULATOR, buildInfoProvider.isEmulator()); final Map sideLoadedInfo = getSideLoadedInfo(); if (sideLoadedInfo != null) { @@ -523,37 +523,6 @@ private TimeZone getTimeZone() { return deviceOrientation; } - /** - * Check whether the application is running in an emulator. - * https://github.com/flutter/plugins/blob/master/packages/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/DeviceInfoPlugin.java#L105 - * - * @return true if the application is running in an emulator, false otherwise - */ - private @Nullable Boolean isEmulator() { - try { - return (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) - || Build.FINGERPRINT.startsWith("generic") - || Build.FINGERPRINT.startsWith("unknown") - || Build.HARDWARE.contains("goldfish") - || Build.HARDWARE.contains("ranchu") - || Build.MODEL.contains("google_sdk") - || Build.MODEL.contains("Emulator") - || Build.MODEL.contains("Android SDK built for x86") - || Build.MANUFACTURER.contains("Genymotion") - || Build.PRODUCT.contains("sdk_google") - || Build.PRODUCT.contains("google_sdk") - || Build.PRODUCT.contains("sdk") - || Build.PRODUCT.contains("sdk_x86") - || Build.PRODUCT.contains("vbox86p") - || Build.PRODUCT.contains("emulator") - || Build.PRODUCT.contains("simulator"); - } catch (Throwable e) { - logger.log( - SentryLevel.ERROR, "Error checking whether application is running in an emulator.", e); - return null; - } - } - /** * Get the total amount of internal storage, in bytes. * diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/IBuildInfoProvider.java b/sentry-android-core/src/main/java/io/sentry/android/core/IBuildInfoProvider.java index e23c4db4b8..b5390e09e1 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/IBuildInfoProvider.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/IBuildInfoProvider.java @@ -19,4 +19,36 @@ public interface IBuildInfoProvider { */ @Nullable String getBuildTags(); + + /** + * Returns the manufacturer of the device + * + * @return the Manufacturer + */ + @Nullable + String getManufacturer(); + + /** + * Returns the model of the device + * + * @return the Build tags + */ + @Nullable + String getModel(); + + /** + * Returns the release version of the device os + * + * @return the Release version + */ + @Nullable + String getVersionRelease(); + + /** + * Check whether the application is running in an emulator. + * + * @return true if the application is running in an emulator, false otherwise + */ + @Nullable + Boolean isEmulator(); } 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 d3f7884195..5b5af60005 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 @@ -55,6 +55,8 @@ final class ManifestMetadataReader { static final String TRACES_ACTIVITY_AUTO_FINISH_ENABLE = "io.sentry.traces.activity.auto-finish.enable"; + static final String TRACES_PROFILING_ENABLE = "io.sentry.traces.profiling.enable"; + @ApiStatus.Experimental static final String TRACE_SAMPLING = "io.sentry.traces.trace-sampling"; static final String TRACING_ORIGINS = "io.sentry.traces.tracing-origins"; @@ -218,6 +220,9 @@ static void applyMetadata( TRACES_ACTIVITY_AUTO_FINISH_ENABLE, options.isEnableActivityLifecycleTracingAutoFinish())); + options.setProfilingEnabled( + readBool(metadata, logger, TRACES_PROFILING_ENABLE, options.isProfilingEnabled())); + final List tracingOrigins = readList(metadata, logger, TRACING_ORIGINS); if (tracingOrigins != null) { for (final String tracingOrigin : tracingOrigins) { diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java index 276061798b..4ee3d47da2 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryAndroidOptions.java @@ -7,6 +7,7 @@ import io.sentry.SpanStatus; import io.sentry.protocol.SdkVersion; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** Sentry SDK options for Android */ public final class SentryAndroidOptions extends SentryOptions { @@ -84,6 +85,12 @@ public final class SentryAndroidOptions extends SentryOptions { */ private boolean enableActivityLifecycleTracingAutoFinish = true; + /** The cache dir. path for caching profiling traces */ + private @Nullable String profilingTracesDirPath; + + /** Interval for profiling traces in milliseconds. Defaults to 300 times per second */ + private int profilingTracesIntervalMillis = 1_000 / 300; + /** Interface that loads the debug images list */ private @NotNull IDebugImagesLoader debugImagesLoader = NoOpDebugImagesLoader.getInstance(); @@ -216,6 +223,42 @@ public void enableAllAutoBreadcrumbs(boolean enable) { enableUserInteractionBreadcrumbs = enable; } + /** + * Returns the profiling traces dir. path if set + * + * @return the profiling traces dir. path or null if not set + */ + public @Nullable String getProfilingTracesDirPath() { + return profilingTracesDirPath; + } + + /** + * Sets the profiling traces dir. path + * + * @param profilingTracesDirPath the profiling traces dir. path + */ + public void setProfilingTracesDirPath(@Nullable String profilingTracesDirPath) { + this.profilingTracesDirPath = profilingTracesDirPath; + } + + /** + * Returns the interval for profiling traces in milliseconds. + * + * @return the interval for profiling traces in milliseconds. + */ + public int getProfilingTracesIntervalMillis() { + return profilingTracesIntervalMillis; + } + + /** + * Sets the interval for profiling traces in milliseconds. + * + * @param profilingTracesIntervalMillis - the interval for profiling traces in milliseconds. + */ + public void setProfilingTracesIntervalMillis(final int profilingTracesIntervalMillis) { + this.profilingTracesIntervalMillis = profilingTracesIntervalMillis; + } + /** * Returns the Debug image loader * diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/CpuInfoUtils.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/CpuInfoUtils.java new file mode 100644 index 0000000000..190cddc4c1 --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/util/CpuInfoUtils.java @@ -0,0 +1,54 @@ +package io.sentry.android.core.internal.util; + +import io.sentry.util.FileUtils; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public final class CpuInfoUtils { + + private static final @NotNull String SYSTEM_CPU_PATH = "/sys/devices/system/cpu/"; + private static final @NotNull String CPUINFO_MAX_FREQ_PATH = "cpufreq/cpuinfo_max_freq"; + + /** Cached max frequencies to avoid reading files multiple times */ + private static @NotNull List cpuMaxFrequenciesMhz = new ArrayList<>(); + + /** + * Read the max frequency of each core of the cpu and returns it in Mhz + * + * @return A list with the frequency of each core of the cpu in Mhz + */ + public static @NotNull List readMaxFrequencies() { + if (!cpuMaxFrequenciesMhz.isEmpty()) { + return cpuMaxFrequenciesMhz; + } + File[] cpuDirs = new File(SYSTEM_CPU_PATH).listFiles(); + if (cpuDirs == null) { + return new ArrayList<>(); + } + + for (File cpuDir : cpuDirs) { + if (!cpuDir.getName().matches("cpu[0-9]+")) continue; + File cpuMaxFreqFile = new File(cpuDir, CPUINFO_MAX_FREQ_PATH); + + if (!cpuMaxFreqFile.exists() || !cpuMaxFreqFile.canRead()) continue; + + long khz = 0; + try { + String content = FileUtils.readText(cpuMaxFreqFile); + if (content == null) continue; + khz = Long.parseLong(content.trim()); + } catch (NumberFormatException e) { + continue; + } catch (IOException e) { + continue; + } + cpuMaxFrequenciesMhz.add(Long.toString(khz / 1000)); + } + return cpuMaxFrequenciesMhz; + } +} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt index 6a5c88ef5a..20b039fdb3 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/ActivityLifecycleIntegrationTest.kt @@ -370,6 +370,8 @@ class ActivityLifecycleIntegrationTest { check { assertEquals(SpanStatus.OK, it.status) }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } @@ -391,6 +393,8 @@ class ActivityLifecycleIntegrationTest { check { assertEquals(SpanStatus.UNKNOWN_ERROR, it.status) }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } @@ -406,7 +410,7 @@ class ActivityLifecycleIntegrationTest { sut.onActivityCreated(activity, fixture.bundle) sut.onActivityPostResumed(activity) - verify(fixture.hub, never()).captureTransaction(any(), anyOrNull()) + verify(fixture.hub, never()).captureTransaction(any(), anyOrNull(), anyOrNull()) } @Test @@ -417,7 +421,7 @@ class ActivityLifecycleIntegrationTest { val activity = mock() sut.onActivityPostResumed(activity) - verify(fixture.hub, never()).captureTransaction(any(), anyOrNull()) + verify(fixture.hub, never()).captureTransaction(any(), anyOrNull(), anyOrNull()) } @Test @@ -430,7 +434,7 @@ class ActivityLifecycleIntegrationTest { sut.onActivityCreated(activity, fixture.bundle) sut.onActivityDestroyed(activity) - verify(fixture.hub).captureTransaction(any(), anyOrNull()) + verify(fixture.hub).captureTransaction(any(), anyOrNull(), anyOrNull(), anyOrNull()) } @Test @@ -499,7 +503,7 @@ class ActivityLifecycleIntegrationTest { sut.onActivityCreated(mock(), mock()) sut.onActivityCreated(mock(), fixture.bundle) - verify(fixture.hub).captureTransaction(any(), anyOrNull()) + verify(fixture.hub).captureTransaction(any(), anyOrNull(), anyOrNull(), anyOrNull()) } @Test @@ -512,7 +516,7 @@ class ActivityLifecycleIntegrationTest { sut.onActivityCreated(activity, mock()) sut.onActivityResumed(activity) - verify(fixture.hub, never()).captureTransaction(any(), any()) + verify(fixture.hub, never()).captureTransaction(any(), any(), anyOrNull()) } @Test @@ -539,7 +543,7 @@ class ActivityLifecycleIntegrationTest { sut.onActivityCreated(activity, mock()) sut.onActivityResumed(activity) - verify(fixture.hub).captureTransaction(any(), anyOrNull()) + verify(fixture.hub).captureTransaction(any(), anyOrNull(), anyOrNull(), anyOrNull()) } @Test 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 ff651b8b83..7b817098be 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 @@ -689,6 +689,31 @@ class ManifestMetadataReaderTest { assertFalse(fixture.options.isTraceSampling) } + @Test + fun `applyMetadata reads enableTracesProfiling to options`() { + // Arrange + val bundle = bundleOf(ManifestMetadataReader.TRACES_PROFILING_ENABLE to true) + val context = fixture.getContext(metaData = bundle) + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options) + + // Assert + assertTrue(fixture.options.isProfilingEnabled) + } + + @Test + fun `applyMetadata reads enableTracesProfiling to options and keeps default`() { + // Arrange + val context = fixture.getContext() + + // Act + ManifestMetadataReader.applyMetadata(context, fixture.options) + + // Assert + assertFalse(fixture.options.isProfilingEnabled) + } + @Test fun `applyMetadata reads tracingOrigins to options`() { // Arrange diff --git a/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt b/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt index 645f9bba30..e830f02e31 100644 --- a/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt +++ b/sentry-apollo/src/test/java/io/sentry/apollo/SentryApolloInterceptorTest.kt @@ -84,6 +84,8 @@ class SentryApolloInterceptorTest { assertTransactionDetails(it) assertEquals(SpanStatus.OK, it.spans.first().status) }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } @@ -97,6 +99,8 @@ class SentryApolloInterceptorTest { assertTransactionDetails(it) assertEquals(SpanStatus.PERMISSION_DENIED, it.spans.first().status) }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } @@ -110,6 +114,8 @@ class SentryApolloInterceptorTest { assertTransactionDetails(it) assertEquals(SpanStatus.INTERNAL_ERROR, it.spans.first().status) }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } @@ -144,6 +150,8 @@ class SentryApolloInterceptorTest { val httpClientSpan = it.spans.first() assertEquals("overwritten description", httpClientSpan.description) }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } @@ -158,6 +166,8 @@ class SentryApolloInterceptorTest { check { assertEquals(1, it.spans.size) }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/tracing/SentryTracingFilterTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/tracing/SentryTracingFilterTest.kt index a8ddd6af21..cbe26816f1 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/tracing/SentryTracingFilterTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/tracing/SentryTracingFilterTest.kt @@ -83,6 +83,8 @@ class SentryTracingFilterTest { assertThat(it.contexts.trace!!.status).isEqualTo(SpanStatus.OK) assertThat(it.contexts.trace!!.operation).isEqualTo("http.server") }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } @@ -97,6 +99,8 @@ class SentryTracingFilterTest { check { assertThat(it.contexts.trace!!.status).isEqualTo(SpanStatus.INTERNAL_ERROR) }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } @@ -111,6 +115,8 @@ class SentryTracingFilterTest { check { assertThat(it.contexts.trace!!.status).isNull() }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } @@ -125,6 +131,8 @@ class SentryTracingFilterTest { check { assertThat(it.contexts.trace!!.parentSpanId).isNull() }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } @@ -140,6 +148,8 @@ class SentryTracingFilterTest { check { assertThat(it.contexts.trace!!.parentSpanId).isEqualTo(parentSpanId) }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } @@ -171,6 +181,8 @@ class SentryTracingFilterTest { check { assertThat(it.status).isEqualTo(SpanStatus.INTERNAL_ERROR) }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/tracing/SentryTransactionAdviceTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/tracing/SentryTransactionAdviceTest.kt index b8f1364a70..3d797ac640 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/tracing/SentryTransactionAdviceTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/tracing/SentryTransactionAdviceTest.kt @@ -63,6 +63,8 @@ class SentryTransactionAdviceTest { assertThat(it.contexts.trace!!.operation).isEqualTo("bean") assertThat(it.status).isEqualTo(SpanStatus.OK) }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } @@ -74,6 +76,8 @@ class SentryTransactionAdviceTest { check { assertThat(it.status).isEqualTo(SpanStatus.INTERNAL_ERROR) }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } @@ -86,6 +90,8 @@ class SentryTransactionAdviceTest { assertThat(it.transaction).isEqualTo("SampleService.methodWithoutTransactionNameSet") assertThat(it.contexts.trace!!.operation).isEqualTo("op") }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } @@ -107,6 +113,8 @@ class SentryTransactionAdviceTest { assertThat(it.transaction).isEqualTo("ClassAnnotatedSampleService.hello") assertThat(it.contexts.trace!!.operation).isEqualTo("op") }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } @@ -119,6 +127,8 @@ class SentryTransactionAdviceTest { assertThat(it.transaction).isEqualTo("ClassAnnotatedWithOperationSampleService.hello") assertThat(it.contexts.trace!!.operation).isEqualTo("my-op") }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 9cc83a2951..065df4a49e 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -171,7 +171,7 @@ public final class io/sentry/Hub : io/sentry/IHub { public fun captureEvent (Lio/sentry/SentryEvent;Ljava/lang/Object;)Lio/sentry/protocol/SentryId; public fun captureException (Ljava/lang/Throwable;Ljava/lang/Object;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; - public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceState;Ljava/lang/Object;)Lio/sentry/protocol/SentryId; + public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceState;Ljava/lang/Object;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V public fun clearBreadcrumbs ()V public fun clone ()Lio/sentry/IHub; @@ -211,7 +211,7 @@ public final class io/sentry/HubAdapter : io/sentry/IHub { public fun captureEvent (Lio/sentry/SentryEvent;Ljava/lang/Object;)Lio/sentry/protocol/SentryId; public fun captureException (Ljava/lang/Throwable;Ljava/lang/Object;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; - public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceState;Ljava/lang/Object;)Lio/sentry/protocol/SentryId; + public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceState;Ljava/lang/Object;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V public fun clearBreadcrumbs ()V public fun clone ()Lio/sentry/IHub; @@ -269,7 +269,8 @@ public abstract interface class io/sentry/IHub { public fun captureMessage (Ljava/lang/String;)Lio/sentry/protocol/SentryId; public abstract fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceState;)Lio/sentry/protocol/SentryId; - public abstract fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceState;Ljava/lang/Object;)Lio/sentry/protocol/SentryId; + public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceState;Ljava/lang/Object;)Lio/sentry/protocol/SentryId; + public abstract fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceState;Ljava/lang/Object;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Ljava/lang/Object;)Lio/sentry/protocol/SentryId; public abstract fun captureUserFeedback (Lio/sentry/UserFeedback;)V public abstract fun clearBreadcrumbs ()V @@ -344,13 +345,20 @@ public abstract interface class io/sentry/ISentryClient { public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Scope;Ljava/lang/Object;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceState;)Lio/sentry/protocol/SentryId; - public abstract fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceState;Lio/sentry/Scope;Ljava/lang/Object;)Lio/sentry/protocol/SentryId; + public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceState;Lio/sentry/Scope;Ljava/lang/Object;)Lio/sentry/protocol/SentryId; + public abstract fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceState;Lio/sentry/Scope;Ljava/lang/Object;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public abstract fun captureUserFeedback (Lio/sentry/UserFeedback;)V public abstract fun close ()V public abstract fun flush (J)V public abstract fun isEnabled ()Z } +public abstract interface class io/sentry/ISentryExecutorService { + public abstract fun close (J)V + public abstract fun schedule (Ljava/lang/Runnable;J)Ljava/util/concurrent/Future; + public abstract fun submit (Ljava/lang/Runnable;)Ljava/util/concurrent/Future; +} + public abstract interface class io/sentry/ISerializer { public abstract fun deserialize (Ljava/io/Reader;Ljava/lang/Class;)Ljava/lang/Object; public abstract fun deserializeEnvelope (Ljava/io/InputStream;)Lio/sentry/SentryEnvelope; @@ -393,6 +401,11 @@ public abstract interface class io/sentry/ITransaction : io/sentry/ISpan { public abstract fun setName (Ljava/lang/String;)V } +public abstract interface class io/sentry/ITransactionProfiler { + public abstract fun onTransactionFinish (Lio/sentry/ITransaction;)Lio/sentry/ProfilingTraceData; + public abstract fun onTransactionStart (Lio/sentry/ITransaction;)V +} + public abstract interface class io/sentry/ITransportFactory { public abstract fun create (Lio/sentry/SentryOptions;Lio/sentry/RequestDetails;)Lio/sentry/transport/ITransport; } @@ -486,7 +499,7 @@ public final class io/sentry/NoOpHub : io/sentry/IHub { public fun captureEvent (Lio/sentry/SentryEvent;Ljava/lang/Object;)Lio/sentry/protocol/SentryId; public fun captureException (Ljava/lang/Throwable;Ljava/lang/Object;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; - public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceState;Ljava/lang/Object;)Lio/sentry/protocol/SentryId; + public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceState;Ljava/lang/Object;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V public fun clearBreadcrumbs ()V public fun clone ()Lio/sentry/IHub; @@ -587,6 +600,12 @@ public final class io/sentry/NoOpTransaction : io/sentry/ITransaction { public fun traceState ()Lio/sentry/TraceState; } +public final class io/sentry/NoOpTransactionProfiler : io/sentry/ITransactionProfiler { + public static fun getInstance ()Lio/sentry/NoOpTransactionProfiler; + public fun onTransactionFinish (Lio/sentry/ITransaction;)Lio/sentry/ProfilingTraceData; + public fun onTransactionStart (Lio/sentry/ITransaction;)V +} + public final class io/sentry/NoOpTransportFactory : io/sentry/ITransportFactory { public fun create (Lio/sentry/SentryOptions;Lio/sentry/RequestDetails;)Lio/sentry/transport/ITransport; public static fun getInstance ()Lio/sentry/NoOpTransportFactory; @@ -603,6 +622,34 @@ public final class io/sentry/OutboxSender : io/sentry/IEnvelopeSender { public fun processEnvelopeFile (Ljava/lang/String;Ljava/lang/Object;)V } +public final class io/sentry/ProfilingTraceData { + public fun (Ljava/io/File;Lio/sentry/ITransaction;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public fun getAndroid_api_level ()I + public fun getBuild_id ()Ljava/lang/String; + public fun getDevice_cpu_frequencies ()Ljava/util/List; + public fun getDevice_locale ()Ljava/lang/String; + public fun getDevice_manufacturer ()Ljava/lang/String; + public fun getDevice_model ()Ljava/lang/String; + public fun getDevice_os_build_number ()Ljava/lang/String; + public fun getDevice_os_name ()Ljava/lang/String; + public fun getDevice_os_version ()Ljava/lang/String; + public fun getDevice_physical_memory_bytes ()Ljava/lang/String; + public fun getDuration_ns ()Ljava/lang/String; + public fun getEnvironment ()Ljava/lang/String; + public fun getPlatform ()Ljava/lang/String; + public fun getProfile_id ()Ljava/lang/String; + public fun getSampled_profile ()Ljava/lang/String; + public fun getTraceFile ()Ljava/io/File; + public fun getTraceId ()Ljava/lang/String; + public fun getTrace_id ()Ljava/lang/String; + public fun getTransaction_id ()Ljava/lang/String; + public fun getTransaction_name ()Ljava/lang/String; + public fun getVersion_code ()Ljava/lang/String; + public fun getVersion_name ()Ljava/lang/String; + public fun isDevice_is_emulator ()Z + public fun setSampled_profile (Ljava/lang/String;)V +} + public final class io/sentry/RequestDetails { public fun (Ljava/lang/String;Ljava/util/Map;)V public fun getHeaders ()Ljava/util/Map; @@ -825,7 +872,7 @@ public final class io/sentry/SentryClient : io/sentry/ISentryClient { public fun captureEnvelope (Lio/sentry/SentryEnvelope;Ljava/lang/Object;)Lio/sentry/protocol/SentryId; public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/Scope;Ljava/lang/Object;)Lio/sentry/protocol/SentryId; public fun captureSession (Lio/sentry/Session;Ljava/lang/Object;)V - public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceState;Lio/sentry/Scope;Ljava/lang/Object;)Lio/sentry/protocol/SentryId; + public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceState;Lio/sentry/Scope;Ljava/lang/Object;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V public fun close ()V public fun flush (J)V @@ -878,6 +925,7 @@ public final class io/sentry/SentryEnvelopeHeader$JsonKeys { public final class io/sentry/SentryEnvelopeItem { public static fun fromAttachment (Lio/sentry/Attachment;J)Lio/sentry/SentryEnvelopeItem; public static fun fromEvent (Lio/sentry/ISerializer;Lio/sentry/SentryBaseEvent;)Lio/sentry/SentryEnvelopeItem; + public static fun fromProfilingTrace (Lio/sentry/ProfilingTraceData;JLio/sentry/ISerializer;)Lio/sentry/SentryEnvelopeItem; public static fun fromSession (Lio/sentry/ISerializer;Lio/sentry/Session;)Lio/sentry/SentryEnvelopeItem; public static fun fromUserFeedback (Lio/sentry/ISerializer;Lio/sentry/UserFeedback;)Lio/sentry/SentryEnvelopeItem; public fun getData ()[B @@ -968,6 +1016,7 @@ public final class io/sentry/SentryEvent$JsonKeys { public final class io/sentry/SentryItemType : java/lang/Enum, io/sentry/JsonSerializable { public static final field Attachment Lio/sentry/SentryItemType; public static final field Event Lio/sentry/SentryItemType; + public static final field Profile Lio/sentry/SentryItemType; public static final field Session Lio/sentry/SentryItemType; public static final field Transaction Lio/sentry/SentryItemType; public static final field Unknown Lio/sentry/SentryItemType; @@ -1012,6 +1061,7 @@ public class io/sentry/SentryOptions { public fun getEnvelopeReader ()Lio/sentry/IEnvelopeReader; public fun getEnvironment ()Ljava/lang/String; public fun getEventProcessors ()Ljava/util/List; + public fun getExecutorService ()Lio/sentry/ISentryExecutorService; public fun getFlushTimeoutMillis ()J public fun getHostnameVerifier ()Ljavax/net/ssl/HostnameVerifier; public fun getIgnoredExceptionsForType ()Ljava/util/Set; @@ -1026,6 +1076,7 @@ public class io/sentry/SentryOptions { public fun getMaxQueueSize ()I public fun getMaxRequestBodySize ()Lio/sentry/SentryOptions$RequestSize; public fun getMaxSpans ()I + public fun getMaxTraceFileSize ()J public fun getOutboxPath ()Ljava/lang/String; public fun getPrintUncaughtStackTrace ()Ljava/lang/Boolean; public fun getProguardUuid ()Ljava/lang/String; @@ -1045,6 +1096,7 @@ public class io/sentry/SentryOptions { public fun getTracesSampleRate ()Ljava/lang/Double; public fun getTracesSampler ()Lio/sentry/SentryOptions$TracesSamplerCallback; public fun getTracingOrigins ()Ljava/util/List; + public fun getTransactionProfiler ()Lio/sentry/ITransactionProfiler; public fun getTransportFactory ()Lio/sentry/ITransportFactory; public fun getTransportGate ()Lio/sentry/transport/ITransportGate; public fun isAttachServerName ()Z @@ -1059,6 +1111,7 @@ public class io/sentry/SentryOptions { public fun isEnableShutdownHook ()Z public fun isEnableUncaughtExceptionHandler ()Z public fun isPrintUncaughtStackTrace ()Z + public fun isProfilingEnabled ()Z public fun isSendDefaultPii ()Z public fun isTraceSampling ()Z public fun isTracingEnabled ()Z @@ -1094,7 +1147,9 @@ public class io/sentry/SentryOptions { public fun setMaxQueueSize (I)V public fun setMaxRequestBodySize (Lio/sentry/SentryOptions$RequestSize;)V public fun setMaxSpans (I)V + public fun setMaxTraceFileSize (J)V public fun setPrintUncaughtStackTrace (Ljava/lang/Boolean;)V + public fun setProfilingEnabled (Z)V public fun setProguardUuid (Ljava/lang/String;)V public fun setProxy (Lio/sentry/SentryOptions$Proxy;)V public fun setReadTimeoutMillis (I)V @@ -1113,6 +1168,7 @@ 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 setTransactionProfiler (Lio/sentry/ITransactionProfiler;)V public fun setTransportFactory (Lio/sentry/ITransportFactory;)V public fun setTransportGate (Lio/sentry/transport/ITransportGate;)V } @@ -2657,6 +2713,12 @@ public final class io/sentry/util/ExceptionUtils { public static fun findRootCause (Ljava/lang/Throwable;)Ljava/lang/Throwable; } +public final class io/sentry/util/FileUtils { + public fun ()V + public static fun deleteRecursively (Ljava/io/File;)Z + public static fun readText (Ljava/io/File;)Ljava/lang/String; +} + public final class io/sentry/util/LogUtils { public fun ()V public static fun logIfNotFlushable (Lio/sentry/ILogger;Ljava/lang/Object;)V diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index 56309475ce..8c57b0f7a1 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -55,7 +55,7 @@ private static void validateOptions(final @NotNull SentryOptions options) { Objects.requireNonNull(options, "SentryOptions is required."); if (options.getDsn() == null || options.getDsn().isEmpty()) { throw new IllegalArgumentException( - "Hub requires a DSN to be instantiated. Considering using the NoOpHub is no DSN is available."); + "Hub requires a DSN to be instantiated. Considering using the NoOpHub if no DSN is available."); } } @@ -538,7 +538,8 @@ public void flush(long timeoutMillis) { public @NotNull SentryId captureTransaction( final @NotNull SentryTransaction transaction, final @Nullable TraceState traceState, - final @Nullable Object hint) { + final @Nullable Object hint, + final @Nullable ProfilingTraceData profilingTraceData) { Objects.requireNonNull(transaction, "transaction is required"); SentryId sentryId = SentryId.EMPTY_ID; @@ -569,7 +570,9 @@ public void flush(long timeoutMillis) { try { item = stack.peek(); sentryId = - item.getClient().captureTransaction(transaction, traceState, item.getScope(), hint); + item.getClient() + .captureTransaction( + transaction, traceState, item.getScope(), hint, profilingTraceData); } catch (Throwable e) { options .getLogger() @@ -658,6 +661,13 @@ public void flush(long timeoutMillis) { startTimestamp, waitForChildren, transactionFinishedCallback); + + // The listener is called only if the transaction exists, as the transaction is needed to + // stop it + if (samplingDecision && options.isProfilingEnabled()) { + final ITransactionProfiler transactionListener = options.getTransactionProfiler(); + transactionListener.onTransactionStart(transaction); + } } if (bindToScope) { configureScope(scope -> scope.setTransaction(transaction)); diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index b0fd237fed..004a89d83e 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -160,8 +160,10 @@ public void flush(long timeoutMillis) { public @NotNull SentryId captureTransaction( @NotNull SentryTransaction transaction, @Nullable TraceState traceState, - @Nullable Object hint) { - return Sentry.getCurrentHub().captureTransaction(transaction, traceState, hint); + @Nullable Object hint, + @Nullable ProfilingTraceData profilingTraceData) { + return Sentry.getCurrentHub() + .captureTransaction(transaction, traceState, hint, profilingTraceData); } @Override diff --git a/sentry/src/main/java/io/sentry/IHub.java b/sentry/src/main/java/io/sentry/IHub.java index 78ba3751e1..a19f1d47a2 100644 --- a/sentry/src/main/java/io/sentry/IHub.java +++ b/sentry/src/main/java/io/sentry/IHub.java @@ -272,6 +272,7 @@ default void addBreadcrumb(@NotNull String message, @NotNull String category) { * @param transaction the transaction * @param traceState the trace state * @param hint the hint + * @param profilingTraceData the profiling trace data * @return transaction's id */ @ApiStatus.Internal @@ -279,7 +280,25 @@ default void addBreadcrumb(@NotNull String message, @NotNull String category) { SentryId captureTransaction( @NotNull SentryTransaction transaction, @Nullable TraceState traceState, - @Nullable Object hint); + @Nullable Object hint, + final @Nullable ProfilingTraceData profilingTraceData); + + /** + * Captures the transaction and enqueues it for sending to Sentry server. + * + * @param transaction the transaction + * @param traceState the trace state + * @param hint the hint + * @return transaction's id + */ + @ApiStatus.Internal + @NotNull + default SentryId captureTransaction( + @NotNull SentryTransaction transaction, + @Nullable TraceState traceState, + @Nullable Object hint) { + return captureTransaction(transaction, traceState, hint, null); + } @ApiStatus.Internal @NotNull diff --git a/sentry/src/main/java/io/sentry/ISentryClient.java b/sentry/src/main/java/io/sentry/ISentryClient.java index b8273d3f8e..a69a808f3d 100644 --- a/sentry/src/main/java/io/sentry/ISentryClient.java +++ b/sentry/src/main/java/io/sentry/ISentryClient.java @@ -216,12 +216,33 @@ default SentryId captureTransaction( * @return The Id (SentryId object) of the event */ @NotNull - @ApiStatus.Experimental + @ApiStatus.Internal + default SentryId captureTransaction( + @NotNull SentryTransaction transaction, + @Nullable TraceState traceState, + @Nullable Scope scope, + @Nullable Object hint) { + return captureTransaction(transaction, traceState, scope, hint, null); + } + + /** + * Captures a transaction. + * + * @param transaction the {@link ITransaction} to send + * @param traceState the trace state + * @param scope An optional scope to be applied to the event. + * @param hint SDK specific but provides high level information about the origin of the event + * @param profilingTraceData An optional profiling trace data captured during the transaction + * @return The Id (SentryId object) of the event + */ + @NotNull + @ApiStatus.Internal SentryId captureTransaction( @NotNull SentryTransaction transaction, @Nullable TraceState traceState, @Nullable Scope scope, - @Nullable Object hint); + @Nullable Object hint, + @Nullable ProfilingTraceData profilingTraceData); /** * Captures a transaction without scope nor hint. @@ -230,7 +251,7 @@ SentryId captureTransaction( * @param traceState the trace state * @return The Id (SentryId object) of the event */ - @ApiStatus.Experimental + @ApiStatus.Internal default @NotNull SentryId captureTransaction( @NotNull SentryTransaction transaction, @Nullable TraceState traceState) { return captureTransaction(transaction, traceState, null, null); diff --git a/sentry/src/main/java/io/sentry/ISentryExecutorService.java b/sentry/src/main/java/io/sentry/ISentryExecutorService.java index dad1f1d1b8..aeaa9a3bc8 100644 --- a/sentry/src/main/java/io/sentry/ISentryExecutorService.java +++ b/sentry/src/main/java/io/sentry/ISentryExecutorService.java @@ -1,10 +1,12 @@ package io.sentry; import java.util.concurrent.Future; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /** Sentry Executor Service that sends cached events and envelopes on App. start. */ -interface ISentryExecutorService { +@ApiStatus.Internal +public interface ISentryExecutorService { /** * Submits a Runnable to the ThreadExecutor @@ -15,6 +17,9 @@ interface ISentryExecutorService { @NotNull Future submit(final @NotNull Runnable runnable); + @NotNull + Future schedule(final @NotNull Runnable runnable, final long delayMillis); + /** * Closes the ThreadExecutor and awaits for the timeout * diff --git a/sentry/src/main/java/io/sentry/ITransactionProfiler.java b/sentry/src/main/java/io/sentry/ITransactionProfiler.java new file mode 100644 index 0000000000..6e6e9cf582 --- /dev/null +++ b/sentry/src/main/java/io/sentry/ITransactionProfiler.java @@ -0,0 +1,14 @@ +package io.sentry; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** Used for performing operations when a transaction is started or ended. */ +@ApiStatus.Internal +public interface ITransactionProfiler { + void onTransactionStart(@NotNull ITransaction transaction); + + @Nullable + ProfilingTraceData onTransactionFinish(@NotNull ITransaction transaction); +} diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index 728e6f5b63..5ca237712a 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -120,7 +120,8 @@ public void flush(long timeoutMillis) {} public @NotNull SentryId captureTransaction( final @NotNull SentryTransaction transaction, final @Nullable TraceState traceState, - final @Nullable Object hint) { + final @Nullable Object hint, + final @Nullable ProfilingTraceData profilingTraceData) { return SentryId.EMPTY_ID; } diff --git a/sentry/src/main/java/io/sentry/NoOpSentryClient.java b/sentry/src/main/java/io/sentry/NoOpSentryClient.java index aace30692b..5b90e7b875 100644 --- a/sentry/src/main/java/io/sentry/NoOpSentryClient.java +++ b/sentry/src/main/java/io/sentry/NoOpSentryClient.java @@ -48,7 +48,8 @@ public SentryId captureEnvelope(@NotNull SentryEnvelope envelope, @Nullable Obje @NotNull SentryTransaction transaction, @Nullable TraceState traceState, @Nullable Scope scope, - @Nullable Object hint) { + @Nullable Object hint, + @Nullable ProfilingTraceData profilingTraceData) { return SentryId.EMPTY_ID; } } diff --git a/sentry/src/main/java/io/sentry/NoOpSentryExecutorService.java b/sentry/src/main/java/io/sentry/NoOpSentryExecutorService.java index 1f2231de3e..dfb323ec4e 100644 --- a/sentry/src/main/java/io/sentry/NoOpSentryExecutorService.java +++ b/sentry/src/main/java/io/sentry/NoOpSentryExecutorService.java @@ -18,6 +18,11 @@ private NoOpSentryExecutorService() {} return new FutureTask<>(() -> null); } + @Override + public @NotNull Future schedule(@NotNull Runnable runnable, long delayMillis) { + return new FutureTask<>(() -> null); + } + @Override public void close(long timeoutMillis) {} } diff --git a/sentry/src/main/java/io/sentry/NoOpTransactionProfiler.java b/sentry/src/main/java/io/sentry/NoOpTransactionProfiler.java new file mode 100644 index 0000000000..2580ded221 --- /dev/null +++ b/sentry/src/main/java/io/sentry/NoOpTransactionProfiler.java @@ -0,0 +1,23 @@ +package io.sentry; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class NoOpTransactionProfiler implements ITransactionProfiler { + + private static final NoOpTransactionProfiler instance = new NoOpTransactionProfiler(); + + private NoOpTransactionProfiler() {} + + public static NoOpTransactionProfiler getInstance() { + return instance; + } + + @Override + public void onTransactionStart(@NotNull ITransaction transaction) {} + + @Override + public @Nullable ProfilingTraceData onTransactionFinish(@NotNull ITransaction transaction) { + return null; + } +} diff --git a/sentry/src/main/java/io/sentry/ProfilingTraceData.java b/sentry/src/main/java/io/sentry/ProfilingTraceData.java new file mode 100644 index 0000000000..9e5eff5dd2 --- /dev/null +++ b/sentry/src/main/java/io/sentry/ProfilingTraceData.java @@ -0,0 +1,192 @@ +package io.sentry; + +import java.io.File; +import java.util.List; +import java.util.Locale; +import java.util.UUID; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class ProfilingTraceData { + + // This field is transient so that it's ignored by Gson + private final transient @NotNull File traceFile; + + // Device metadata + private final int android_api_level; + private final @NotNull String device_locale; + private final @NotNull String device_manufacturer; + private final @NotNull String device_model; + private final @NotNull String device_os_build_number; + private final @NotNull String device_os_name; + private final @NotNull String device_os_version; + private final boolean device_is_emulator; + private final @NotNull List device_cpu_frequencies; + private final @NotNull String device_physical_memory_bytes; + private final @NotNull String platform; + private final @NotNull String build_id; + + // Transaction info + private final @NotNull String transaction_name; + // duration_ns is a String to avoid issues with numbers and json + private final @NotNull String duration_ns; + + // App info + private final @NotNull String version_name; + private final @NotNull String version_code; + + // Stacktrace context + private final @NotNull String transaction_id; + private final @NotNull String trace_id; + private final @NotNull String profile_id; + private final @NotNull String environment; + + // Stacktrace (file) + /** Profile trace encoded with Base64 */ + private @Nullable String sampled_profile = null; + + public ProfilingTraceData( + @NotNull File traceFile, + @NotNull ITransaction transaction, + @NotNull String durationNanos, + int sdkInt, + @Nullable String deviceManufacturer, + @Nullable String deviceModel, + @Nullable String deviceOsVersion, + @Nullable Boolean deviceIsEmulator, + @NotNull List deviceCpuFrequencies, + @Nullable String devicePhysicalMemoryBytes, + @Nullable String buildId, + @Nullable String versionName, + @Nullable String versionCode, + @Nullable String environment) { + this.traceFile = traceFile; + + // Device metadata + this.android_api_level = sdkInt; + this.device_locale = Locale.getDefault().toString(); + this.device_manufacturer = deviceManufacturer != null ? deviceManufacturer : ""; + this.device_model = deviceModel != null ? deviceModel : ""; + this.device_os_version = deviceOsVersion != null ? deviceOsVersion : ""; + this.device_is_emulator = deviceIsEmulator != null ? deviceIsEmulator : false; + this.device_cpu_frequencies = deviceCpuFrequencies; + this.device_physical_memory_bytes = + devicePhysicalMemoryBytes != null ? devicePhysicalMemoryBytes : "0"; + this.device_os_build_number = ""; + this.device_os_name = "android"; + this.platform = "android"; + this.build_id = buildId != null ? buildId : ""; + + // Transaction info + this.transaction_name = transaction.getName(); + this.duration_ns = durationNanos; + + // App info + this.version_name = versionName != null ? versionName : ""; + this.version_code = versionCode != null ? versionCode : ""; + + // Stacktrace context + this.transaction_id = transaction.getEventId().toString(); + this.trace_id = transaction.getSpanContext().getTraceId().toString(); + this.profile_id = UUID.randomUUID().toString(); + this.environment = environment != null ? environment : ""; + } + + public @NotNull File getTraceFile() { + return traceFile; + } + + public @NotNull String getTraceId() { + return trace_id; + } + + public int getAndroid_api_level() { + return android_api_level; + } + + public @NotNull String getDevice_locale() { + return device_locale; + } + + public @NotNull String getDevice_manufacturer() { + return device_manufacturer; + } + + public @NotNull String getDevice_model() { + return device_model; + } + + public @NotNull String getDevice_os_build_number() { + return device_os_build_number; + } + + public @NotNull String getDevice_os_name() { + return device_os_name; + } + + public @NotNull String getDevice_os_version() { + return device_os_version; + } + + public boolean isDevice_is_emulator() { + return device_is_emulator; + } + + public @NotNull String getPlatform() { + return platform; + } + + public @NotNull String getBuild_id() { + return build_id; + } + + public @NotNull String getTransaction_name() { + return transaction_name; + } + + public @NotNull String getVersion_name() { + return version_name; + } + + public @NotNull String getVersion_code() { + return version_code; + } + + public @NotNull String getTransaction_id() { + return transaction_id; + } + + public @NotNull String getTrace_id() { + return trace_id; + } + + public @NotNull String getProfile_id() { + return profile_id; + } + + public @NotNull String getEnvironment() { + return environment; + } + + public @Nullable String getSampled_profile() { + return sampled_profile; + } + + public @NotNull String getDuration_ns() { + return duration_ns; + } + + public @NotNull List getDevice_cpu_frequencies() { + return device_cpu_frequencies; + } + + public @NotNull String getDevice_physical_memory_bytes() { + return device_physical_memory_bytes; + } + + public void setSampled_profile(@Nullable String sampledProfile) { + this.sampled_profile = sampledProfile; + } +} diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index d69ea36323..55244c679b 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -1,5 +1,6 @@ package io.sentry; +import io.sentry.exception.SentryEnvelopeException; import io.sentry.hints.DiskFlushNotification; import io.sentry.protocol.Contexts; import io.sentry.protocol.SentryId; @@ -133,12 +134,12 @@ private boolean shouldApplyScopeData( ? scope.getTransaction().traceState() : null; final SentryEnvelope envelope = - buildEnvelope(event, getAttachmentsFromScope(scope), session, traceState); + buildEnvelope(event, getAttachmentsFromScope(scope), session, traceState, null); if (envelope != null) { transport.send(envelope, hint); } - } catch (IOException e) { + } catch (IOException | SentryEnvelopeException e) { options.getLogger().log(SentryLevel.WARNING, e, "Capturing event %s failed.", sentryId); // if there was an error capturing the event, we return an emptyId @@ -160,8 +161,9 @@ private boolean shouldApplyScopeData( final @Nullable SentryBaseEvent event, final @Nullable List attachments, final @Nullable Session session, - final @Nullable TraceState traceState) - throws IOException { + final @Nullable TraceState traceState, + final @Nullable ProfilingTraceData profilingTraceData) + throws IOException, SentryEnvelopeException { SentryId sentryId = null; final List envelopeItems = new ArrayList<>(); @@ -179,6 +181,13 @@ private boolean shouldApplyScopeData( envelopeItems.add(sessionItem); } + if (profilingTraceData != null) { + final SentryEnvelopeItem profilingTraceItem = + SentryEnvelopeItem.fromProfilingTrace( + profilingTraceData, options.getMaxTraceFileSize(), options.getSerializer()); + envelopeItems.add(profilingTraceItem); + } + if (attachments != null) { for (final Attachment attachment : attachments) { final SentryEnvelopeItem attachmentItem = @@ -401,7 +410,8 @@ public void captureSession(final @NotNull Session session, final @Nullable Objec @NotNull SentryTransaction transaction, @Nullable TraceState traceState, final @Nullable Scope scope, - final @Nullable Object hint) { + final @Nullable Object hint, + final @Nullable ProfilingTraceData profilingTraceData) { Objects.requireNonNull(transaction, "Transaction is required."); options @@ -437,13 +447,18 @@ public void captureSession(final @NotNull Session session, final @Nullable Objec try { final SentryEnvelope envelope = buildEnvelope( - transaction, filterForTransaction(getAttachmentsFromScope(scope)), null, traceState); + transaction, + filterForTransaction(getAttachmentsFromScope(scope)), + null, + traceState, + profilingTraceData); + if (envelope != null) { transport.send(envelope, hint); } else { sentryId = SentryId.EMPTY_ID; } - } catch (IOException e) { + } catch (IOException | SentryEnvelopeException e) { options.getLogger().log(SentryLevel.WARNING, e, "Capturing transaction %s failed.", sentryId); // if there was an error capturing the event, we return an emptyId sentryId = SentryId.EMPTY_ID; diff --git a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java index d16c688135..b110f6491c 100644 --- a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java +++ b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java @@ -1,8 +1,12 @@ package io.sentry; +import static io.sentry.vendor.Base64.NO_PADDING; +import static io.sentry.vendor.Base64.NO_WRAP; + import io.sentry.exception.SentryEnvelopeException; import io.sentry.protocol.SentryTransaction; import io.sentry.util.Objects; +import io.sentry.vendor.Base64; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.BufferedWriter; @@ -179,51 +183,8 @@ public static SentryEnvelopeItem fromAttachment( } return attachment.getBytes(); } else if (attachment.getPathname() != null) { - - try { - File file = new File(attachment.getPathname()); - - if (!file.isFile()) { - throw new SentryEnvelopeException( - String.format( - "Reading the attachment %s failed, because the file located at the path is not a file.", - attachment.getPathname())); - } - - if (!file.canRead()) { - throw new SentryEnvelopeException( - String.format( - "Reading the attachment %s failed, because can't read the file.", - attachment.getPathname())); - } - - if (file.length() > maxAttachmentSize) { - throw new SentryEnvelopeException( - String.format( - "Dropping attachment, because the size of the it located at " - + "'%s' with %d bytes is bigger than the maximum " - + "allowed attachment size of %d bytes.", - attachment.getPathname(), file.length(), maxAttachmentSize)); - } - - try (FileInputStream fileInputStream = - new FileInputStream(attachment.getPathname()); - BufferedInputStream inputStream = new BufferedInputStream(fileInputStream); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { - byte[] bytes = new byte[1024]; - int length; - int offset = 0; - while ((length = inputStream.read(bytes)) != -1) { - outputStream.write(bytes, offset, length); - } - return outputStream.toByteArray(); - } - } catch (IOException | SecurityException exception) { - throw new SentryEnvelopeException( - String.format("Reading the attachment %s failed.", attachment.getPathname())); - } + return readBytesFromFile(attachment.getPathname(), maxAttachmentSize); } - throw new SentryEnvelopeException( String.format( "Couldn't attach the attachment %s.\n" @@ -243,6 +204,96 @@ public static SentryEnvelopeItem fromAttachment( return new SentryEnvelopeItem(itemHeader, () -> cachedItem.getBytes()); } + public static @Nullable SentryEnvelopeItem fromProfilingTrace( + final @NotNull ProfilingTraceData profilingTraceData, + final long maxTraceFileSize, + final @NotNull ISerializer serializer) + throws SentryEnvelopeException { + + File traceFile = profilingTraceData.getTraceFile(); + if (!traceFile.exists()) { + return null; + } + + // Using CachedItem, so we read the trace file in the background + final CachedItem cachedItem = + new CachedItem( + () -> { + if (!traceFile.exists()) { + return null; + } + // 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); + profilingTraceData.setSampled_profile(base64Trace); + + try (final ByteArrayOutputStream stream = new ByteArrayOutputStream(); + final Writer writer = new BufferedWriter(new OutputStreamWriter(stream, UTF_8))) { + serializer.serialize(profilingTraceData, writer); + return stream.toByteArray(); + } catch (IOException e) { + throw new SentryEnvelopeException( + String.format("Failed to serialize profiling trace data\n%s", e.getMessage())); + } finally { + // In any case we delete the trace file + traceFile.delete(); + } + }); + + SentryEnvelopeItemHeader itemHeader = + new SentryEnvelopeItemHeader( + SentryItemType.Profile, + () -> cachedItem.getBytes().length, + "application-json", + traceFile.getName()); + + // Don't use method reference. This can cause issues on Android + return new SentryEnvelopeItem(itemHeader, () -> cachedItem.getBytes()); + } + + private static byte[] readBytesFromFile(String pathname, long maxFileLength) + throws SentryEnvelopeException { + try { + File file = new File(pathname); + + if (!file.isFile()) { + throw new SentryEnvelopeException( + String.format( + "Reading the item %s failed, because the file located at the path is not a file.", + pathname)); + } + + if (!file.canRead()) { + throw new SentryEnvelopeException( + String.format("Reading the item %s failed, because can't read the file.", pathname)); + } + + if (file.length() > maxFileLength) { + throw new SentryEnvelopeException( + String.format( + "Dropping item, because its size located at '%s' with %d bytes is bigger " + + "than the maximum allowed size of %d bytes.", + pathname, file.length(), maxFileLength)); + } + + try (FileInputStream fileInputStream = new FileInputStream(pathname); + BufferedInputStream inputStream = new BufferedInputStream(fileInputStream); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + byte[] bytes = new byte[1024]; + int length; + int offset = 0; + while ((length = inputStream.read(bytes)) != -1) { + outputStream.write(bytes, offset, length); + } + return outputStream.toByteArray(); + } + } catch (IOException | SecurityException exception) { + throw new SentryEnvelopeException( + String.format("Reading the item %s failed.\n%s", pathname, exception.getMessage())); + } + } + private static class CachedItem { private @Nullable byte[] bytes; private final @Nullable Callable dataFactory; diff --git a/sentry/src/main/java/io/sentry/SentryExecutorService.java b/sentry/src/main/java/io/sentry/SentryExecutorService.java index dd2faa29ab..d388dfd726 100644 --- a/sentry/src/main/java/io/sentry/SentryExecutorService.java +++ b/sentry/src/main/java/io/sentry/SentryExecutorService.java @@ -1,23 +1,23 @@ package io.sentry; -import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.TestOnly; final class SentryExecutorService implements ISentryExecutorService { - private final @NotNull ExecutorService executorService; + private final @NotNull ScheduledExecutorService executorService; @TestOnly - SentryExecutorService(final @NotNull ExecutorService executorService) { + SentryExecutorService(final @NotNull ScheduledExecutorService executorService) { this.executorService = executorService; } SentryExecutorService() { - this(Executors.newSingleThreadExecutor()); + this(Executors.newSingleThreadScheduledExecutor()); } @Override @@ -25,6 +25,11 @@ final class SentryExecutorService implements ISentryExecutorService { return executorService.submit(runnable); } + @Override + public @NotNull Future schedule(final @NotNull Runnable runnable, final long delayMillis) { + return executorService.schedule(runnable, delayMillis, TimeUnit.MILLISECONDS); + } + @Override public void close(final long timeoutMillis) { synchronized (executorService) { diff --git a/sentry/src/main/java/io/sentry/SentryItemType.java b/sentry/src/main/java/io/sentry/SentryItemType.java index 52f32ce9a2..8282099095 100644 --- a/sentry/src/main/java/io/sentry/SentryItemType.java +++ b/sentry/src/main/java/io/sentry/SentryItemType.java @@ -13,6 +13,7 @@ public enum SentryItemType implements JsonSerializable { UserFeedback("user_report"), // Sentry backend still uses user_report Attachment("attachment"), Transaction("transaction"), + Profile("profile"), Unknown("__unknown__"); // DataCategory.Unknown private final String itemType; diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 901c47a59e..1bd14bb12b 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -289,6 +289,15 @@ public class SentryOptions { /** Controls if the `tracestate` header is attached to envelopes and HTTP client integrations. */ private boolean traceSampling; + /** Control if profiling is enabled or not for transactions */ + private boolean profilingEnabled = false; + + /** Max trace file size in bytes. */ + private long maxTraceFileSize = 5 * 1024 * 1024; + + /** Listener interface to perform operations when a transaction is started or ended */ + private @NotNull ITransactionProfiler transactionProfiler = NoOpTransactionProfiler.getInstance(); + /** * Contains a list of origins to which `sentry-trace` header should be sent in HTTP integrations. */ @@ -1019,8 +1028,9 @@ public void setPrintUncaughtStackTrace(final @Nullable Boolean printUncaughtStac * * @return the SentryExecutorService */ + @ApiStatus.Internal @NotNull - ISentryExecutorService getExecutorService() { + public ISentryExecutorService getExecutorService() { return executorService; } @@ -1406,6 +1416,61 @@ public void setTraceSampling(boolean traceSampling) { this.traceSampling = traceSampling; } + /** + * Returns the maximum trace file size for each envelope item in bytes. + * + * @return the maximum attachment size in bytes. + */ + public long getMaxTraceFileSize() { + return maxTraceFileSize; + } + + /** + * Sets the max trace file size for each envelope item in bytes. Default is 5 Mb. + * + * @param maxTraceFileSize the max trace file size in bytes. + */ + public void setMaxTraceFileSize(long maxTraceFileSize) { + this.maxTraceFileSize = maxTraceFileSize; + } + + /** + * Returns the listener interface to perform operations when a transaction is started or ended. + * + * @return the listener interface to perform operations when a transaction is started or ended. + */ + public @NotNull ITransactionProfiler getTransactionProfiler() { + return transactionProfiler; + } + + /** + * Sets the listener interface to perform operations when a transaction is started or ended. + * + * @param transactionProfiler - the listener for operations when a transaction is started or ended + */ + public void setTransactionProfiler(final @Nullable ITransactionProfiler transactionProfiler) { + this.transactionProfiler = + transactionProfiler != null ? transactionProfiler : NoOpTransactionProfiler.getInstance(); + } + + /** + * Returns if profiling is enabled for transactions. + * + * @return if profiling is enabled for transactions. + */ + public boolean isProfilingEnabled() { + return profilingEnabled; + } + + /** + * Sets whether profiling is enabled for transactions. + * + * @param profilingEnabled - whether profiling is enabled for transactions + */ + public void setProfilingEnabled(boolean profilingEnabled) { + this.profilingEnabled = profilingEnabled; + } + /** * Returns a list of origins to which `sentry-trace` header should be sent in HTTP integrations. * @@ -1569,6 +1634,9 @@ void merge(final @NotNull ExternalOptions options) { if (options.getEnableDeduplication() != null) { setEnableDeduplication(options.getEnableDeduplication()); } + setTransactionProfiler(options.getTransactionProfiler()); + setMaxTraceFileSize(options.getMaxTraceFileSize()); + setProfilingEnabled(options.isProfilingEnabled()); final Map tags = new HashMap<>(options.getTags()); for (final Map.Entry tag : tags.entrySet()) { this.tags.put(tag.getKey(), tag.getValue()); diff --git a/sentry/src/main/java/io/sentry/SentryTracer.java b/sentry/src/main/java/io/sentry/SentryTracer.java index 3f1bd0b4cf..f46ab96256 100644 --- a/sentry/src/main/java/io/sentry/SentryTracer.java +++ b/sentry/src/main/java/io/sentry/SentryTracer.java @@ -203,6 +203,10 @@ public void finish() { public void finish(@Nullable SpanStatus status) { this.finishStatus = FinishStatus.finishing(status); if (!root.isFinished() && (!waitForChildren || hasAllChildrenFinished())) { + ProfilingTraceData profilingTraceData = null; + if (hub.getOptions().isProfilingEnabled()) { + profilingTraceData = hub.getOptions().getTransactionProfiler().onTransactionFinish(this); + } root.finish(finishStatus.spanStatus); // finish unfinished children @@ -236,7 +240,7 @@ public void finish(@Nullable SpanStatus status) { if (transactionFinishedCallback != null) { transactionFinishedCallback.execute(this); } - hub.captureTransaction(transaction, this.traceState()); + hub.captureTransaction(transaction, this.traceState(), null, profilingTraceData); } } diff --git a/sentry/src/main/java/io/sentry/transport/ReusableCountLatch.java b/sentry/src/main/java/io/sentry/transport/ReusableCountLatch.java index bb79cfced2..1193d3c88f 100644 --- a/sentry/src/main/java/io/sentry/transport/ReusableCountLatch.java +++ b/sentry/src/main/java/io/sentry/transport/ReusableCountLatch.java @@ -16,9 +16,9 @@ *

A {@code ReusableCountLatch} is initialized with a given count. The {@link * #waitTillZero} methods block until the current count reaches zero due to invocations of the * {@link #decrement} method, after which all waiting threads are released. If zero has been reached - * any subsequent invocations of {@link #waitTillZero} return immediately. The coun cen be increased - * calling the {@link #increment()} method and any subsequent thread calling the {@link - * #waitTillZero} method will be block again until another zero is reached. + * any subsequent invocations of {@link #waitTillZero} return immediately. The count can be + * increased calling the {@link #increment()} method and any subsequent thread calling the {@link + * #waitTillZero} method will be blocked again until another zero is reached. * *

{@code ReusableCountLatch} provides more versatility than {@link * java.util.concurrent.CountDownLatch CountDownLatch} as the count doesn't have to be known upfront @@ -27,7 +27,7 @@ * instead can count up to 2_147_483_647 (2^31-1). * *

Great use case for {@code ReusableCountLatch} is when you wait for tasks on other threads to - * finish, but these tasks could trigger more tasks and it is not know upfront how many will be + * finish, but these tasks could trigger more tasks and it is not known upfront how many will be * triggered in total. * * @author mtymes diff --git a/sentry/src/main/java/io/sentry/util/FileUtils.java b/sentry/src/main/java/io/sentry/util/FileUtils.java new file mode 100644 index 0000000000..b5ac3f8a1a --- /dev/null +++ b/sentry/src/main/java/io/sentry/util/FileUtils.java @@ -0,0 +1,59 @@ +package io.sentry.util; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class FileUtils { + + /** + * Deletes the file or directory denoted by a path. If it is a directory, all files and directory + * inside it are deleted recursively. Note that if this operation fails then partial deletion may + * have taken place. + * + * @param file file or directory to delete + * @return true if the file/directory is successfully deleted, false otherwise + */ + public static boolean deleteRecursively(@Nullable File file) { + if (file == null || !file.exists()) { + return true; + } + if (file.isFile()) { + return file.delete(); + } + File[] children = file.listFiles(); + if (children == null) return true; + for (File f : children) { + if (!deleteRecursively(f)) return false; + } + return file.delete(); + } + + /** + * Reads the content of a File into a String. If the file does not exist or is not a file, null is + * returned. Do not use with large files, as the String is kept in memory! + * + * @param file file to read + * @return a String containing all the content of the file, or null if it doesn't exists + * @throws IOException In case of error reading the file + */ + @SuppressWarnings("DefaultCharset") + public static @Nullable String readText(@Nullable File file) throws IOException { + if (file == null || !file.exists() || !file.isFile()) { + return null; + } + StringBuilder contentBuilder = new StringBuilder(); + try (BufferedReader br = new BufferedReader(new FileReader(file))) { + + String line; + while ((line = br.readLine()) != null) { + contentBuilder.append(line).append("\n"); + } + } + return contentBuilder.toString(); + } +} diff --git a/sentry/src/test/java/io/sentry/HubTest.kt b/sentry/src/test/java/io/sentry/HubTest.kt index b36722364f..38701d1498 100644 --- a/sentry/src/test/java/io/sentry/HubTest.kt +++ b/sentry/src/test/java/io/sentry/HubTest.kt @@ -54,7 +54,7 @@ class HubTest { @Test fun `when no dsn available, ctor throws illegal arg`() { val ex = assertFailsWith { Hub(SentryOptions()) } - assertEquals("Hub requires a DSN to be instantiated. Considering using the NoOpHub is no DSN is available.", ex.message) + assertEquals("Hub requires a DSN to be instantiated. Considering using the NoOpHub if no DSN is available.", ex.message) } @Test @@ -1084,7 +1084,7 @@ class HubTest { val sentryTracer = SentryTracer(TransactionContext("name", "op", true), sut) sentryTracer.finish() val traceState = sentryTracer.traceState() - verify(mockClient).captureTransaction(any(), eq(traceState), any(), eq(null)) + verify(mockClient).captureTransaction(any(), eq(traceState), any(), eq(null), eq(null)) } @Test diff --git a/sentry/src/test/java/io/sentry/OutboxSenderTest.kt b/sentry/src/test/java/io/sentry/OutboxSenderTest.kt index a244c45d73..57cd2c313b 100644 --- a/sentry/src/test/java/io/sentry/OutboxSenderTest.kt +++ b/sentry/src/test/java/io/sentry/OutboxSenderTest.kt @@ -88,6 +88,7 @@ class OutboxSenderTest { fixture.envelopeReader = EnvelopeReader(JsonSerializer(fixture.options)) whenever(fixture.options.maxSpans).thenReturn(1000) whenever(fixture.hub.options).thenReturn(fixture.options) + whenever(fixture.options.transactionProfiler).thenReturn(NoOpTransactionProfiler.getInstance()) val transactionContext = TransactionContext("fixture-name", "http") transactionContext.description = "fixture-request" diff --git a/sentry/src/test/java/io/sentry/SentryEnvelopeItemTest.kt b/sentry/src/test/java/io/sentry/SentryEnvelopeItemTest.kt index e320d82dd1..f985a61945 100644 --- a/sentry/src/test/java/io/sentry/SentryEnvelopeItemTest.kt +++ b/sentry/src/test/java/io/sentry/SentryEnvelopeItemTest.kt @@ -197,9 +197,9 @@ class SentryEnvelopeItemTest { } assertEquals( - "Dropping attachment, because the size of the it located at " + + "Dropping item, because its size located at " + "'${fixture.pathname}' with ${file.length()} bytes is bigger than the maximum " + - "allowed attachment size of ${fixture.maxAttachmentSize} bytes.", + "allowed size of ${fixture.maxAttachmentSize} bytes.", exception.message ) } diff --git a/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt b/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt index 64d2ad7d37..6f65d50508 100644 --- a/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt +++ b/sentry/src/test/java/io/sentry/SentryExecutorServiceTest.kt @@ -6,8 +6,8 @@ import com.nhaarman.mockitokotlin2.never import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.whenever import org.awaitility.kotlin.await -import java.util.concurrent.ExecutorService import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.atomic.AtomicBoolean import kotlin.test.Test import kotlin.test.assertTrue @@ -16,7 +16,7 @@ class SentryExecutorServiceTest { @Test fun `SentryExecutorService forwards submit call to ExecutorService`() { - val executor = mock() + val executor = mock() val sentryExecutor = SentryExecutorService(executor) sentryExecutor.submit {} verify(executor).submit(any()) @@ -24,7 +24,7 @@ class SentryExecutorServiceTest { @Test fun `SentryExecutorService forwards close call to ExecutorService`() { - val executor = mock() + val executor = mock() val sentryExecutor = SentryExecutorService(executor) whenever(executor.isShutdown).thenReturn(false) whenever(executor.awaitTermination(any(), any())).thenReturn(true) @@ -34,7 +34,7 @@ class SentryExecutorServiceTest { @Test fun `SentryExecutorService forwards close and call shutdownNow if not enough time`() { - val executor = mock() + val executor = mock() val sentryExecutor = SentryExecutorService(executor) whenever(executor.isShutdown).thenReturn(false) whenever(executor.awaitTermination(any(), any())).thenReturn(false) @@ -44,7 +44,7 @@ class SentryExecutorServiceTest { @Test fun `SentryExecutorService forwards close and call shutdownNow if await throws`() { - val executor = mock() + val executor = mock() val sentryExecutor = SentryExecutorService(executor) whenever(executor.isShutdown).thenReturn(false) whenever(executor.awaitTermination(any(), any())).thenThrow(InterruptedException()) @@ -54,7 +54,7 @@ class SentryExecutorServiceTest { @Test fun `SentryExecutorService forwards close but do not shutdown if its already closed`() { - val executor = mock() + val executor = mock() val sentryExecutor = SentryExecutorService(executor) whenever(executor.isShutdown).thenReturn(true) sentryExecutor.close(15000) @@ -63,7 +63,7 @@ class SentryExecutorServiceTest { @Test fun `SentryExecutorService forwards close call to ExecutorService and close it`() { - val executor = Executors.newSingleThreadExecutor() + val executor = Executors.newSingleThreadScheduledExecutor() val sentryExecutor = SentryExecutorService(executor) sentryExecutor.close(15000) assertTrue(executor.isShutdown) diff --git a/sentry/src/test/java/io/sentry/SentryTracerTest.kt b/sentry/src/test/java/io/sentry/SentryTracerTest.kt index 29da535ee2..75bdbc60f0 100644 --- a/sentry/src/test/java/io/sentry/SentryTracerTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTracerTest.kt @@ -8,11 +8,14 @@ import com.nhaarman.mockitokotlin2.never import com.nhaarman.mockitokotlin2.spy import com.nhaarman.mockitokotlin2.times import com.nhaarman.mockitokotlin2.verify +import io.sentry.protocol.App +import io.sentry.protocol.Request import io.sentry.protocol.User import java.util.Date import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertNotEquals import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertSame @@ -118,6 +121,8 @@ class SentryTracerTest { check { assertEquals(it.transaction, tracer.name) }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } @@ -153,6 +158,8 @@ class SentryTracerTest { assertEquals(emptyMap(), it.tags) } }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } @@ -169,6 +176,8 @@ class SentryTracerTest { assertEquals(1, it.spans.size) assertEquals("op1", it.spans.first().op) }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } @@ -300,6 +309,8 @@ class SentryTracerTest { assertEquals(SpanStatus.OK, it.status) } }, + anyOrNull(), + anyOrNull(), anyOrNull() ) @@ -342,7 +353,7 @@ class SentryTracerTest { val transaction = fixture.getSut(waitForChildren = true) transaction.startChild("op") transaction.finish() - verify(fixture.hub, never()).captureTransaction(any(), any()) + verify(fixture.hub, never()).captureTransaction(any(), any(), anyOrNull(), anyOrNull()) } @Test @@ -351,7 +362,7 @@ class SentryTracerTest { val child = transaction.startChild("op") child.finish() transaction.finish() - verify(fixture.hub).captureTransaction(any(), anyOrNull()) + verify(fixture.hub).captureTransaction(any(), anyOrNull(), anyOrNull(), anyOrNull()) } @Test @@ -372,7 +383,7 @@ class SentryTracerTest { val transaction = fixture.getSut(waitForChildren = true) val child = transaction.startChild("op") child.finish() - verify(fixture.hub, never()).captureTransaction(any(), any()) + verify(fixture.hub, never()).captureTransaction(any(), any(), anyOrNull(), anyOrNull()) } @Test @@ -380,12 +391,14 @@ class SentryTracerTest { val transaction = fixture.getSut(waitForChildren = true) val child = transaction.startChild("op") transaction.finish(SpanStatus.INVALID_ARGUMENT) - verify(fixture.hub, never()).captureTransaction(any(), any()) + verify(fixture.hub, never()).captureTransaction(any(), any(), anyOrNull(), anyOrNull()) child.finish() verify(fixture.hub, times(1)).captureTransaction( check { assertEquals(SpanStatus.INVALID_ARGUMENT, it.status) }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } @@ -404,6 +417,8 @@ class SentryTracerTest { assertEquals(SpanStatus.DEADLINE_EXCEEDED, it.spans[0].status) assertEquals(SpanStatus.DEADLINE_EXCEEDED, it.spans[1].status) }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } @@ -472,6 +487,8 @@ class SentryTracerTest { check { assertEquals("val", it.getExtra("key")) }, + anyOrNull(), + anyOrNull(), anyOrNull() ) } @@ -489,6 +506,8 @@ class SentryTracerTest { assertEquals("val", it["key"]) } }, + anyOrNull(), + anyOrNull(), anyOrNull() ) }