From a3d904d7f4ba9b50aea92281ab79d036e01921de Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Tue, 17 Sep 2024 15:39:20 +0200 Subject: [PATCH] added profile_chunk envelope create added IHub.captureProfileChunk and ISentryClient.captureProfileChunk added profilerId and chunkId reset logic to AndroidContinuousProfiler added absolute timestamps to ProfileMeasurementValue added ProfileContext to Contexts --- .../core/AndroidContinuousProfiler.java | 56 +++- .../android/core/AndroidCpuCollector.java | 5 +- .../android/core/AndroidMemoryCollector.java | 4 +- .../sentry/android/core/AndroidProfiler.java | 21 +- .../android/core/AndroidCpuCollectorTest.kt | 1 + .../core/AndroidMemoryCollectorTest.kt | 1 + .../android/core/AndroidProfilerTest.kt | 9 +- .../core/AndroidTransactionProfilerTest.kt | 6 +- .../core/SessionTrackingIntegrationTest.kt | 5 + sentry/api/sentry.api | 95 +++++- .../java/io/sentry/CpuCollectionData.java | 12 +- sentry/src/main/java/io/sentry/Hub.java | 30 ++ .../src/main/java/io/sentry/HubAdapter.java | 6 + sentry/src/main/java/io/sentry/IHub.java | 10 + .../main/java/io/sentry/ISentryClient.java | 11 + .../java/io/sentry/JavaMemoryCollector.java | 3 +- .../main/java/io/sentry/JsonSerializer.java | 2 + .../java/io/sentry/MainEventProcessor.java | 31 +- .../java/io/sentry/MemoryCollectionData.java | 17 +- sentry/src/main/java/io/sentry/NoOpHub.java | 5 + .../main/java/io/sentry/NoOpSentryClient.java | 6 + .../src/main/java/io/sentry/ProfileChunk.java | 317 ++++++++++++++++++ .../main/java/io/sentry/ProfileContext.java | 122 +++++++ .../src/main/java/io/sentry/SentryClient.java | 40 ++- .../java/io/sentry/SentryEnvelopeItem.java | 62 +++- .../main/java/io/sentry/SentryItemType.java | 1 + .../src/main/java/io/sentry/SpanContext.java | 6 + .../ProfileMeasurementValue.java | 43 ++- .../java/io/sentry/protocol/Contexts.java | 15 + .../java/io/sentry/protocol/DebugMeta.java | 38 +++ .../src/test/java/io/sentry/HubAdapterTest.kt | 6 + sentry/src/test/java/io/sentry/HubTest.kt | 46 +++ .../java/io/sentry/JavaMemoryCollectorTest.kt | 1 + .../test/java/io/sentry/JsonSerializerTest.kt | 262 +++++++++++++-- sentry/src/test/java/io/sentry/NoOpHubTest.kt | 4 + .../java/io/sentry/NoOpSentryClientTest.kt | 4 + .../sentry/PerformanceCollectionDataTest.kt | 15 +- .../test/java/io/sentry/SentryClientTest.kt | 57 +++- .../java/io/sentry/SentryEnvelopeItemTest.kt | 88 +++++ .../java/io/sentry/protocol/ContextsTest.kt | 6 + .../java/io/sentry/protocol/DebugMetaTest.kt | 72 ++++ 41 files changed, 1454 insertions(+), 87 deletions(-) create mode 100644 sentry/src/main/java/io/sentry/ProfileChunk.java create mode 100644 sentry/src/main/java/io/sentry/ProfileContext.java diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java index 8c980be7a2..c6b1ab9dd4 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java @@ -8,8 +8,13 @@ import io.sentry.IHub; import io.sentry.ILogger; import io.sentry.ISentryExecutorService; +import io.sentry.ProfileChunk; import io.sentry.SentryLevel; +import io.sentry.SentryOptions; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; +import io.sentry.protocol.SentryId; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.Future; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -32,6 +37,9 @@ public class AndroidContinuousProfiler implements IContinuousProfiler { private boolean isRunning = false; private @Nullable IHub hub; private @Nullable Future closeFuture; + private final @NotNull List payloadBuilders = new ArrayList<>(); + private @NotNull SentryId profilerId = SentryId.EMPTY_ID; + private @NotNull SentryId chunkId = SentryId.EMPTY_ID; public AndroidContinuousProfiler( final @NotNull BuildInfoProvider buildInfoProvider, @@ -105,8 +113,17 @@ public synchronized void start() { if (startData == null) { return; } + isRunning = true; + if (profilerId == SentryId.EMPTY_ID) { + profilerId = new SentryId(); + } + + if (chunkId == SentryId.EMPTY_ID) { + chunkId = new SentryId(); + } + closeFuture = executorService.schedule(() -> stop(true), MAX_CHUNK_DURATION_MILLIS); } @@ -138,14 +155,29 @@ private synchronized void stop(final boolean restartProfiler) { return; } + // The hub can be null if the profiler is started before the SDK is initialized (app start + // profiling), meaning there's no hub to send the chunks. In that case, we store the data in a + // list and send it when the next chunk is finished. + synchronized (payloadBuilders) { + payloadBuilders.add( + new ProfileChunk.Builder( + profilerId, chunkId, endData.measurementsMap, endData.traceFile)); + } + isRunning = false; + // A chunk is finished. Next chunk will have a different id. + chunkId = SentryId.EMPTY_ID; - // todo schedule capture profile chunk envelope + if (hub != null) { + sendChunks(hub, hub.getOptions()); + } if (restartProfiler) { logger.log(SentryLevel.DEBUG, "Profile chunk finished. Starting a new one."); start(); } else { + // When the profiler is stopped manually, we have to reset its id + profilerId = SentryId.EMPTY_ID; logger.log(SentryLevel.DEBUG, "Profile chunk finished."); } } @@ -157,6 +189,28 @@ public synchronized void close() { stop(); } + private void sendChunks(final @NotNull IHub hub, final @NotNull SentryOptions options) { + try { + options + .getExecutorService() + .submit( + () -> { + final ArrayList payloads = new ArrayList<>(payloadBuilders.size()); + synchronized (payloadBuilders) { + for (ProfileChunk.Builder builder : payloadBuilders) { + payloads.add(builder.build(options)); + } + payloadBuilders.clear(); + } + for (ProfileChunk payload : payloads) { + hub.captureProfileChunk(payload); + } + }); + } catch (Throwable e) { + options.getLogger().log(SentryLevel.DEBUG, "Failed to send profile chunks.", e); + } + } + @Override public boolean isRunning() { return isRunning; diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidCpuCollector.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidCpuCollector.java index 8f54305e6f..2c9ef38cbb 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidCpuCollector.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidCpuCollector.java @@ -10,6 +10,7 @@ import io.sentry.IPerformanceSnapshotCollector; import io.sentry.PerformanceCollectionData; import io.sentry.SentryLevel; +import io.sentry.SentryNanotimeDate; import io.sentry.util.FileUtils; import io.sentry.util.Objects; import java.io.File; @@ -85,7 +86,9 @@ public void collect(final @NotNull PerformanceCollectionData performanceCollecti CpuCollectionData cpuData = new CpuCollectionData( - System.currentTimeMillis(), (cpuUsagePercentage / (double) numCores) * 100.0); + System.currentTimeMillis(), + (cpuUsagePercentage / (double) numCores) * 100.0, + new SentryNanotimeDate()); performanceCollectionData.addCpuData(cpuData); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMemoryCollector.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMemoryCollector.java index f475c1801b..866a1db1a4 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMemoryCollector.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMemoryCollector.java @@ -4,6 +4,7 @@ import io.sentry.IPerformanceSnapshotCollector; import io.sentry.MemoryCollectionData; import io.sentry.PerformanceCollectionData; +import io.sentry.SentryNanotimeDate; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -18,7 +19,8 @@ public void collect(final @NotNull PerformanceCollectionData performanceCollecti long now = System.currentTimeMillis(); long usedMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); long usedNativeMemory = Debug.getNativeHeapSize() - Debug.getNativeHeapFreeSize(); - MemoryCollectionData memoryData = new MemoryCollectionData(now, usedMemory, usedNativeMemory); + MemoryCollectionData memoryData = + new MemoryCollectionData(now, usedMemory, usedNativeMemory, new SentryNanotimeDate()); performanceCollectionData.addMemoryData(memoryData); } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java index 3d2e8a9205..75d507246c 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java @@ -11,7 +11,9 @@ import io.sentry.ISentryExecutorService; import io.sentry.MemoryCollectionData; import io.sentry.PerformanceCollectionData; +import io.sentry.SentryDate; import io.sentry.SentryLevel; +import io.sentry.SentryNanotimeDate; import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.profilemeasurements.ProfileMeasurement; import io.sentry.profilemeasurements.ProfileMeasurementValue; @@ -158,6 +160,7 @@ public void onFrameMetricCollected( // profileStartNanos is calculated through SystemClock.elapsedRealtimeNanos(), // but frameEndNanos uses System.nanotime(), so we convert it to get the timestamp // relative to profileStartNanos + final SentryDate timestamp = new SentryNanotimeDate(); final long frameTimestampRelativeNanos = frameEndNanos - System.nanoTime() @@ -171,15 +174,18 @@ public void onFrameMetricCollected( } if (isFrozen) { frozenFrameRenderMeasurements.addLast( - new ProfileMeasurementValue(frameTimestampRelativeNanos, durationNanos)); + new ProfileMeasurementValue( + frameTimestampRelativeNanos, durationNanos, timestamp)); } else if (isSlow) { slowFrameRenderMeasurements.addLast( - new ProfileMeasurementValue(frameTimestampRelativeNanos, durationNanos)); + new ProfileMeasurementValue( + frameTimestampRelativeNanos, durationNanos, timestamp)); } if (refreshRate != lastRefreshRate) { lastRefreshRate = refreshRate; screenFrameRateMeasurements.addLast( - new ProfileMeasurementValue(frameTimestampRelativeNanos, refreshRate)); + new ProfileMeasurementValue( + frameTimestampRelativeNanos, refreshRate, timestamp)); } } }); @@ -326,19 +332,22 @@ private void putPerformanceCollectionDataInMeasurements( cpuUsageMeasurements.add( new ProfileMeasurementValue( TimeUnit.MILLISECONDS.toNanos(cpuData.getTimestampMillis()) + timestampDiff, - cpuData.getCpuUsagePercentage())); + cpuData.getCpuUsagePercentage(), + cpuData.getTimestamp())); } if (memoryData != null && memoryData.getUsedHeapMemory() > -1) { memoryUsageMeasurements.add( new ProfileMeasurementValue( TimeUnit.MILLISECONDS.toNanos(memoryData.getTimestampMillis()) + timestampDiff, - memoryData.getUsedHeapMemory())); + memoryData.getUsedHeapMemory(), + memoryData.getTimestamp())); } if (memoryData != null && memoryData.getUsedNativeMemory() > -1) { nativeMemoryUsageMeasurements.add( new ProfileMeasurementValue( TimeUnit.MILLISECONDS.toNanos(memoryData.getTimestampMillis()) + timestampDiff, - memoryData.getUsedNativeMemory())); + memoryData.getUsedNativeMemory(), + memoryData.getTimestamp())); } } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidCpuCollectorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidCpuCollectorTest.kt index 45010255e8..5df457c700 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidCpuCollectorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidCpuCollectorTest.kt @@ -56,6 +56,7 @@ class AndroidCpuCollectorTest { assertNotNull(cpuData) assertNotEquals(0.0, cpuData.cpuUsagePercentage) assertNotEquals(0, cpuData.timestampMillis) + assertNotEquals(0, cpuData.timestamp.nanoTimestamp()) } @Test diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidMemoryCollectorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidMemoryCollectorTest.kt index 7879c2daf5..3f00775d69 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidMemoryCollectorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidMemoryCollectorTest.kt @@ -28,5 +28,6 @@ class AndroidMemoryCollectorTest { assertEquals(usedNativeMemory, memoryData.usedNativeMemory) assertEquals(usedMemory, memoryData.usedHeapMemory) assertNotEquals(0, memoryData.timestampMillis) + assertNotEquals(0, memoryData.timestamp.nanoTimestamp()) } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt index c5bb334bb3..e9d11decb0 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidProfilerTest.kt @@ -9,6 +9,7 @@ import io.sentry.ILogger import io.sentry.ISentryExecutorService import io.sentry.MemoryCollectionData import io.sentry.PerformanceCollectionData +import io.sentry.SentryDate import io.sentry.SentryExecutorService import io.sentry.SentryLevel import io.sentry.android.core.internal.util.SentryFrameMetricsCollector @@ -278,12 +279,14 @@ class AndroidProfilerTest { val profiler = fixture.getSut() val performanceCollectionData = ArrayList() var singleData = PerformanceCollectionData() - singleData.addMemoryData(MemoryCollectionData(1, 2, 3)) - singleData.addCpuData(CpuCollectionData(1, 1.4)) + val t1 = mock() + val t2 = mock() + singleData.addMemoryData(MemoryCollectionData(1, 2, 3, t1)) + singleData.addCpuData(CpuCollectionData(1, 1.4, t1)) performanceCollectionData.add(singleData) singleData = PerformanceCollectionData() - singleData.addMemoryData(MemoryCollectionData(2, 3, 4)) + singleData.addMemoryData(MemoryCollectionData(2, 3, 4, t2)) performanceCollectionData.add(singleData) profiler.start() diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt index 02cda7d23b..fd944b6cb6 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidTransactionProfilerTest.kt @@ -459,12 +459,12 @@ class AndroidTransactionProfilerTest { val profiler = fixture.getSut(context) val performanceCollectionData = ArrayList() var singleData = PerformanceCollectionData() - singleData.addMemoryData(MemoryCollectionData(1, 2, 3)) - singleData.addCpuData(CpuCollectionData(1, 1.4)) + singleData.addMemoryData(MemoryCollectionData(1, 2, 3, mock())) + singleData.addCpuData(CpuCollectionData(1, 1.4, mock())) performanceCollectionData.add(singleData) singleData = PerformanceCollectionData() - singleData.addMemoryData(MemoryCollectionData(2, 3, 4)) + singleData.addMemoryData(MemoryCollectionData(2, 3, 4, mock())) performanceCollectionData.add(singleData) profiler.start() diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt index e6d3dfadd7..3707aebc62 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt @@ -11,6 +11,7 @@ import io.sentry.Hint import io.sentry.IMetricsAggregator import io.sentry.IScope import io.sentry.ISentryClient +import io.sentry.ProfileChunk import io.sentry.ProfilingTraceData import io.sentry.Sentry import io.sentry.SentryEnvelope @@ -177,6 +178,10 @@ class SessionTrackingIntegrationTest { TODO("Not yet implemented") } + override fun captureProfileChunk(profileChunk: ProfileChunk, scope: IScope?): SentryId { + TODO("Not yet implemented") + } + override fun captureCheckIn(checkIn: CheckIn, scope: IScope?, hint: Hint?): SentryId { TODO("Not yet implemented") } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index bc4e522708..f089f08dd9 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -210,8 +210,9 @@ public final class io/sentry/CheckInStatus : java/lang/Enum { } public final class io/sentry/CpuCollectionData { - public fun (JD)V + public fun (JDLio/sentry/SentryDate;)V public fun getCpuUsagePercentage ()D + public fun getTimestamp ()Lio/sentry/SentryDate; public fun getTimestampMillis ()J } @@ -438,6 +439,7 @@ public final class io/sentry/Hub : io/sentry/IHub, io/sentry/metrics/MetricsApi$ public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureProfileChunk (Lio/sentry/ProfileChunk;)Lio/sentry/protocol/SentryId; public fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V @@ -495,6 +497,7 @@ public final class io/sentry/HubAdapter : io/sentry/IHub { public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureProfileChunk (Lio/sentry/ProfileChunk;)Lio/sentry/protocol/SentryId; public fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V @@ -594,6 +597,7 @@ public abstract interface class io/sentry/IHub { public fun captureMessage (Ljava/lang/String;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public abstract fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public abstract fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public abstract fun captureProfileChunk (Lio/sentry/ProfileChunk;)Lio/sentry/protocol/SentryId; public abstract fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;)Lio/sentry/protocol/SentryId; @@ -775,6 +779,7 @@ public abstract interface class io/sentry/ISentryClient { public fun captureException (Ljava/lang/Throwable;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; + public abstract fun captureProfileChunk (Lio/sentry/ProfileChunk;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; public abstract fun captureReplayEvent (Lio/sentry/SentryReplayEvent;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureSession (Lio/sentry/Session;)V public abstract fun captureSession (Lio/sentry/Session;Lio/sentry/Hint;)V @@ -1069,8 +1074,9 @@ public final class io/sentry/MeasurementUnit$Information : java/lang/Enum, io/se } public final class io/sentry/MemoryCollectionData { - public fun (JJ)V - public fun (JJJ)V + public fun (JJJLio/sentry/SentryDate;)V + public fun (JJLio/sentry/SentryDate;)V + public fun getTimestamp ()Lio/sentry/SentryDate; public fun getTimestampMillis ()J public fun getUsedHeapMemory ()J public fun getUsedNativeMemory ()J @@ -1222,6 +1228,7 @@ public final class io/sentry/NoOpHub : io/sentry/IHub { public fun captureException (Ljava/lang/Throwable;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;)Lio/sentry/protocol/SentryId; public fun captureMessage (Ljava/lang/String;Lio/sentry/SentryLevel;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; + public fun captureProfileChunk (Lio/sentry/ProfileChunk;)Lio/sentry/protocol/SentryId; public fun captureReplay (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; public fun captureUserFeedback (Lio/sentry/UserFeedback;)V @@ -1531,6 +1538,78 @@ public final class io/sentry/PerformanceCollectionData { public fun getMemoryData ()Lio/sentry/MemoryCollectionData; } +public final class io/sentry/ProfileChunk : io/sentry/JsonSerializable, io/sentry/JsonUnknown { + public fun ()V + public fun (Lio/sentry/protocol/SentryId;Lio/sentry/protocol/SentryId;Ljava/io/File;Ljava/util/Map;Lio/sentry/SentryOptions;)V + public fun equals (Ljava/lang/Object;)Z + public fun getChunkId ()Lio/sentry/protocol/SentryId; + public fun getClientSdk ()Lio/sentry/protocol/SdkVersion; + public fun getDebugMeta ()Lio/sentry/protocol/DebugMeta; + public fun getEnvironment ()Ljava/lang/String; + public fun getMeasurements ()Ljava/util/Map; + public fun getPlatform ()Ljava/lang/String; + public fun getProfilerId ()Lio/sentry/protocol/SentryId; + public fun getRelease ()Ljava/lang/String; + public fun getSampledProfile ()Ljava/lang/String; + public fun getTraceFile ()Ljava/io/File; + public fun getUnknown ()Ljava/util/Map; + public fun getVersion ()Ljava/lang/String; + public fun hashCode ()I + public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public fun setDebugMeta (Lio/sentry/protocol/DebugMeta;)V + public fun setSampledProfile (Ljava/lang/String;)V + public fun setUnknown (Ljava/util/Map;)V +} + +public final class io/sentry/ProfileChunk$Builder { + public fun (Lio/sentry/protocol/SentryId;Lio/sentry/protocol/SentryId;Ljava/util/Map;Ljava/io/File;)V + public fun build (Lio/sentry/SentryOptions;)Lio/sentry/ProfileChunk; +} + +public final class io/sentry/ProfileChunk$Deserializer : io/sentry/JsonDeserializer { + public fun ()V + public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/ProfileChunk; + public synthetic fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; +} + +public final class io/sentry/ProfileChunk$JsonKeys { + public static final field CHUNK_ID Ljava/lang/String; + public static final field CLIENT_SDK Ljava/lang/String; + public static final field DEBUG_META Ljava/lang/String; + public static final field ENVIRONMENT Ljava/lang/String; + public static final field MEASUREMENTS Ljava/lang/String; + public static final field PLATFORM Ljava/lang/String; + public static final field PROFILER_ID Ljava/lang/String; + public static final field RELEASE Ljava/lang/String; + public static final field SAMPLED_PROFILE Ljava/lang/String; + public static final field VERSION Ljava/lang/String; + public fun ()V +} + +public class io/sentry/ProfileContext : io/sentry/JsonSerializable, io/sentry/JsonUnknown { + public static final field TYPE Ljava/lang/String; + public fun ()V + public fun (Lio/sentry/ProfileContext;)V + public fun (Lio/sentry/protocol/SentryId;)V + public fun equals (Ljava/lang/Object;)Z + public fun getProfilerId ()Lio/sentry/protocol/SentryId; + public fun getUnknown ()Ljava/util/Map; + public fun hashCode ()I + public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public fun setUnknown (Ljava/util/Map;)V +} + +public final class io/sentry/ProfileContext$Deserializer : io/sentry/JsonDeserializer { + public fun ()V + public fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Lio/sentry/ProfileContext; + public synthetic fun deserialize (Lio/sentry/ObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; +} + +public final class io/sentry/ProfileContext$JsonKeys { + public static final field PROFILER_ID Ljava/lang/String; + public fun ()V +} + public final class io/sentry/ProfilingTraceData : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public static final field TRUNCATION_REASON_BACKGROUNDED Ljava/lang/String; public static final field TRUNCATION_REASON_NORMAL Ljava/lang/String; @@ -2054,6 +2133,7 @@ public final class io/sentry/SentryClient : io/sentry/ISentryClient, io/sentry/m public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureMetrics (Lio/sentry/metrics/EncodedMetrics;)Lio/sentry/protocol/SentryId; + public fun captureProfileChunk (Lio/sentry/ProfileChunk;Lio/sentry/IScope;)Lio/sentry/protocol/SentryId; public fun captureReplayEvent (Lio/sentry/SentryReplayEvent;Lio/sentry/IScope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureSession (Lio/sentry/Session;Lio/sentry/Hint;)V public fun captureTransaction (Lio/sentry/protocol/SentryTransaction;Lio/sentry/TraceContext;Lio/sentry/IScope;Lio/sentry/Hint;Lio/sentry/ProfilingTraceData;)Lio/sentry/protocol/SentryId; @@ -2135,6 +2215,7 @@ public final class io/sentry/SentryEnvelopeItem { public static fun fromClientReport (Lio/sentry/ISerializer;Lio/sentry/clientreport/ClientReport;)Lio/sentry/SentryEnvelopeItem; public static fun fromEvent (Lio/sentry/ISerializer;Lio/sentry/SentryBaseEvent;)Lio/sentry/SentryEnvelopeItem; public static fun fromMetrics (Lio/sentry/metrics/EncodedMetrics;)Lio/sentry/SentryEnvelopeItem; + public static fun fromProfileChunk (Lio/sentry/ProfileChunk;Lio/sentry/ISerializer;)Lio/sentry/SentryEnvelopeItem; public static fun fromProfilingTrace (Lio/sentry/ProfilingTraceData;JLio/sentry/ISerializer;)Lio/sentry/SentryEnvelopeItem; public static fun fromReplay (Lio/sentry/ISerializer;Lio/sentry/ILogger;Lio/sentry/SentryReplayEvent;Lio/sentry/ReplayRecording;Z)Lio/sentry/SentryEnvelopeItem; public static fun fromSession (Lio/sentry/ISerializer;Lio/sentry/Session;)Lio/sentry/SentryEnvelopeItem; @@ -2265,6 +2346,7 @@ public final class io/sentry/SentryItemType : java/lang/Enum, io/sentry/JsonSeri public static final field ClientReport Lio/sentry/SentryItemType; public static final field Event Lio/sentry/SentryItemType; public static final field Profile Lio/sentry/SentryItemType; + public static final field ProfileChunk Lio/sentry/SentryItemType; public static final field ReplayEvent Lio/sentry/SentryItemType; public static final field ReplayRecording Lio/sentry/SentryItemType; public static final field ReplayVideo Lio/sentry/SentryItemType; @@ -3896,9 +3978,10 @@ public final class io/sentry/profilemeasurements/ProfileMeasurement$JsonKeys { public final class io/sentry/profilemeasurements/ProfileMeasurementValue : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun ()V - public fun (Ljava/lang/Long;Ljava/lang/Number;)V + public fun (Ljava/lang/Long;Ljava/lang/Number;Lio/sentry/SentryDate;)V public fun equals (Ljava/lang/Object;)Z public fun getRelativeStartNs ()Ljava/lang/String; + public fun getTimestamp ()Ljava/lang/Double; public fun getUnknown ()Ljava/util/Map; public fun getValue ()D public fun hashCode ()I @@ -3914,6 +3997,7 @@ public final class io/sentry/profilemeasurements/ProfileMeasurementValue$Deseria public final class io/sentry/profilemeasurements/ProfileMeasurementValue$JsonKeys { public static final field START_NS Ljava/lang/String; + public static final field TIMESTAMP Ljava/lang/String; public static final field VALUE Ljava/lang/String; public fun ()V } @@ -4006,6 +4090,7 @@ public final class io/sentry/protocol/Contexts : java/util/concurrent/Concurrent public fun getDevice ()Lio/sentry/protocol/Device; public fun getGpu ()Lio/sentry/protocol/Gpu; public fun getOperatingSystem ()Lio/sentry/protocol/OperatingSystem; + public fun getProfile ()Lio/sentry/ProfileContext; public fun getResponse ()Lio/sentry/protocol/Response; public fun getRuntime ()Lio/sentry/protocol/SentryRuntime; public fun getTrace ()Lio/sentry/SpanContext; @@ -4015,6 +4100,7 @@ public final class io/sentry/protocol/Contexts : java/util/concurrent/Concurrent public fun setDevice (Lio/sentry/protocol/Device;)V public fun setGpu (Lio/sentry/protocol/Gpu;)V public fun setOperatingSystem (Lio/sentry/protocol/OperatingSystem;)V + public fun setProfile (Lio/sentry/ProfileContext;)V public fun setResponse (Lio/sentry/protocol/Response;)V public fun setRuntime (Lio/sentry/protocol/SentryRuntime;)V public fun setTrace (Lio/sentry/SpanContext;)V @@ -4076,6 +4162,7 @@ public final class io/sentry/protocol/DebugImage$JsonKeys { public final class io/sentry/protocol/DebugMeta : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun ()V + public static fun buildDebugMeta (Lio/sentry/protocol/DebugMeta;Lio/sentry/SentryOptions;)Lio/sentry/protocol/DebugMeta; public fun getImages ()Ljava/util/List; public fun getSdkInfo ()Lio/sentry/protocol/SdkInfo; public fun getUnknown ()Ljava/util/Map; diff --git a/sentry/src/main/java/io/sentry/CpuCollectionData.java b/sentry/src/main/java/io/sentry/CpuCollectionData.java index 081063a53f..cf011a1e5c 100644 --- a/sentry/src/main/java/io/sentry/CpuCollectionData.java +++ b/sentry/src/main/java/io/sentry/CpuCollectionData.java @@ -1,15 +1,25 @@ package io.sentry; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; @ApiStatus.Internal public final class CpuCollectionData { final long timestampMillis; final double cpuUsagePercentage; + final @NotNull SentryDate timestamp; - public CpuCollectionData(final long timestampMillis, final double cpuUsagePercentage) { + public CpuCollectionData( + final long timestampMillis, + final double cpuUsagePercentage, + final @NotNull SentryDate timestamp) { this.timestampMillis = timestampMillis; this.cpuUsagePercentage = cpuUsagePercentage; + this.timestamp = timestamp; + } + + public @NotNull SentryDate getTimestamp() { + return timestamp; } public long getTimestampMillis() { diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index 240c6b54f2..376cfcffec 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -725,6 +725,36 @@ public void flush(long timeoutMillis) { return sentryId; } + @ApiStatus.Internal + @Override + public @NotNull SentryId captureProfileChunk( + final @NotNull ProfileChunk profilingContinuousData) { + Objects.requireNonNull(profilingContinuousData, "profilingContinuousData is required"); + + @NotNull SentryId sentryId = SentryId.EMPTY_ID; + if (!isEnabled()) { + options + .getLogger() + .log( + SentryLevel.WARNING, + "Instance is disabled and this 'captureTransaction' call is a no-op."); + } else { + try { + final @NotNull StackItem item = stack.peek(); + sentryId = item.getClient().captureProfileChunk(profilingContinuousData, item.getScope()); + } catch (Throwable e) { + options + .getLogger() + .log( + SentryLevel.ERROR, + "Error while capturing profile chunk with id: " + + profilingContinuousData.getChunkId(), + e); + } + } + return sentryId; + } + @Override public @NotNull ITransaction startTransaction( final @NotNull TransactionContext transactionContext, diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index d5adc4da80..95551eaa3f 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -206,6 +206,12 @@ public void flush(long timeoutMillis) { return Sentry.startTransaction(transactionContext, transactionOptions); } + @Override + public @NotNull SentryId captureProfileChunk( + final @NotNull ProfileChunk profilingContinuousData) { + return Sentry.getCurrentHub().captureProfileChunk(profilingContinuousData); + } + @Deprecated @Override public @Nullable SentryTraceHeader traceHeaders() { diff --git a/sentry/src/main/java/io/sentry/IHub.java b/sentry/src/main/java/io/sentry/IHub.java index 6ae5a00925..6b6bd6d102 100644 --- a/sentry/src/main/java/io/sentry/IHub.java +++ b/sentry/src/main/java/io/sentry/IHub.java @@ -412,6 +412,16 @@ default SentryId captureTransaction(@NotNull SentryTransaction transaction, @Nul return captureTransaction(transaction, traceContext, null); } + /** + * Captures the profile chunk and enqueues it for sending to Sentry server. + * + * @param profileChunk the continuous profiling payload + * @return the profile chunk id + */ + @ApiStatus.Internal + @NotNull + SentryId captureProfileChunk(final @NotNull ProfileChunk profileChunk); + /** * Creates a Transaction and returns the instance. * diff --git a/sentry/src/main/java/io/sentry/ISentryClient.java b/sentry/src/main/java/io/sentry/ISentryClient.java index 8d1815b4c8..f9772a79cf 100644 --- a/sentry/src/main/java/io/sentry/ISentryClient.java +++ b/sentry/src/main/java/io/sentry/ISentryClient.java @@ -277,6 +277,17 @@ SentryId captureTransaction( return captureTransaction(transaction, null, null, null); } + /** + * Captures the profile chunk and enqueues it for sending to Sentry server. + * + * @param profilingContinuousData the continuous profiling payload + * @return the profile chunk id + */ + @ApiStatus.Internal + @NotNull + SentryId captureProfileChunk( + final @NotNull ProfileChunk profilingContinuousData, final @Nullable IScope scope); + @NotNull @ApiStatus.Experimental SentryId captureCheckIn(@NotNull CheckIn checkIn, @Nullable IScope scope, @Nullable Hint hint); diff --git a/sentry/src/main/java/io/sentry/JavaMemoryCollector.java b/sentry/src/main/java/io/sentry/JavaMemoryCollector.java index cdde808ba5..603c452364 100644 --- a/sentry/src/main/java/io/sentry/JavaMemoryCollector.java +++ b/sentry/src/main/java/io/sentry/JavaMemoryCollector.java @@ -15,7 +15,8 @@ public void setup() {} public void collect(final @NotNull PerformanceCollectionData performanceCollectionData) { final long now = System.currentTimeMillis(); final long usedMemory = runtime.totalMemory() - runtime.freeMemory(); - MemoryCollectionData memoryData = new MemoryCollectionData(now, usedMemory); + MemoryCollectionData memoryData = + new MemoryCollectionData(now, usedMemory, new SentryNanotimeDate()); performanceCollectionData.addMemoryData(memoryData); } } diff --git a/sentry/src/main/java/io/sentry/JsonSerializer.java b/sentry/src/main/java/io/sentry/JsonSerializer.java index 6c46306cc7..181a8d7638 100644 --- a/sentry/src/main/java/io/sentry/JsonSerializer.java +++ b/sentry/src/main/java/io/sentry/JsonSerializer.java @@ -91,6 +91,8 @@ public JsonSerializer(@NotNull SentryOptions options) { deserializersByClass.put(Message.class, new Message.Deserializer()); deserializersByClass.put(MetricSummary.class, new MetricSummary.Deserializer()); deserializersByClass.put(OperatingSystem.class, new OperatingSystem.Deserializer()); + deserializersByClass.put(ProfileChunk.class, new ProfileChunk.Deserializer()); + deserializersByClass.put(ProfileContext.class, new ProfileContext.Deserializer()); deserializersByClass.put(ProfilingTraceData.class, new ProfilingTraceData.Deserializer()); deserializersByClass.put( ProfilingTransactionData.class, new ProfilingTransactionData.Deserializer()); diff --git a/sentry/src/main/java/io/sentry/MainEventProcessor.java b/sentry/src/main/java/io/sentry/MainEventProcessor.java index d6445e3a56..ac5d85ce27 100644 --- a/sentry/src/main/java/io/sentry/MainEventProcessor.java +++ b/sentry/src/main/java/io/sentry/MainEventProcessor.java @@ -2,7 +2,6 @@ import io.sentry.hints.AbnormalExit; import io.sentry.hints.Cached; -import io.sentry.protocol.DebugImage; import io.sentry.protocol.DebugMeta; import io.sentry.protocol.SentryException; import io.sentry.protocol.SentryTransaction; @@ -65,34 +64,8 @@ public MainEventProcessor(final @NotNull SentryOptions options) { } private void setDebugMeta(final @NotNull SentryBaseEvent event) { - final @NotNull List debugImages = new ArrayList<>(); - - if (options.getProguardUuid() != null) { - final DebugImage proguardMappingImage = new DebugImage(); - proguardMappingImage.setType(DebugImage.PROGUARD); - proguardMappingImage.setUuid(options.getProguardUuid()); - debugImages.add(proguardMappingImage); - } - - for (final @NotNull String bundleId : options.getBundleIds()) { - final DebugImage sourceBundleImage = new DebugImage(); - sourceBundleImage.setType(DebugImage.JVM); - sourceBundleImage.setDebugId(bundleId); - debugImages.add(sourceBundleImage); - } - - if (!debugImages.isEmpty()) { - DebugMeta debugMeta = event.getDebugMeta(); - - if (debugMeta == null) { - debugMeta = new DebugMeta(); - } - if (debugMeta.getImages() == null) { - debugMeta.setImages(debugImages); - } else { - debugMeta.getImages().addAll(debugImages); - } - + final DebugMeta debugMeta = DebugMeta.buildDebugMeta(event.getDebugMeta(), options); + if (debugMeta != null) { event.setDebugMeta(debugMeta); } } diff --git a/sentry/src/main/java/io/sentry/MemoryCollectionData.java b/sentry/src/main/java/io/sentry/MemoryCollectionData.java index 0fbb66412e..6a85fa28b0 100644 --- a/sentry/src/main/java/io/sentry/MemoryCollectionData.java +++ b/sentry/src/main/java/io/sentry/MemoryCollectionData.java @@ -1,22 +1,33 @@ package io.sentry; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; @ApiStatus.Internal public final class MemoryCollectionData { final long timestampMillis; final long usedHeapMemory; final long usedNativeMemory; + final @NotNull SentryDate timestamp; public MemoryCollectionData( - final long timestampMillis, final long usedHeapMemory, final long usedNativeMemory) { + final long timestampMillis, + final long usedHeapMemory, + final long usedNativeMemory, + final @NotNull SentryDate timestamp) { this.timestampMillis = timestampMillis; this.usedHeapMemory = usedHeapMemory; this.usedNativeMemory = usedNativeMemory; + this.timestamp = timestamp; } - public MemoryCollectionData(final long timestampMillis, final long usedHeapMemory) { - this(timestampMillis, usedHeapMemory, -1); + public MemoryCollectionData( + final long timestampMillis, final long usedHeapMemory, final @NotNull SentryDate timestamp) { + this(timestampMillis, usedHeapMemory, -1, timestamp); + } + + public @NotNull SentryDate getTimestamp() { + return timestamp; } public long getTimestampMillis() { diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index 88488fbda0..e6781a2177 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -160,6 +160,11 @@ public void flush(long timeoutMillis) {} return SentryId.EMPTY_ID; } + @Override + public @NotNull SentryId captureProfileChunk(final @NotNull ProfileChunk profileChunk) { + return SentryId.EMPTY_ID; + } + @Override public @NotNull ITransaction startTransaction( @NotNull TransactionContext transactionContext, diff --git a/sentry/src/main/java/io/sentry/NoOpSentryClient.java b/sentry/src/main/java/io/sentry/NoOpSentryClient.java index f00f309544..5b4b67c0bd 100644 --- a/sentry/src/main/java/io/sentry/NoOpSentryClient.java +++ b/sentry/src/main/java/io/sentry/NoOpSentryClient.java @@ -59,6 +59,12 @@ public SentryId captureEnvelope(@NotNull SentryEnvelope envelope, @Nullable Hint return SentryId.EMPTY_ID; } + @Override + public @NotNull SentryId captureProfileChunk( + final @NotNull ProfileChunk profileChunk, final @Nullable IScope scope) { + return SentryId.EMPTY_ID; + } + @Override @ApiStatus.Experimental public @NotNull SentryId captureCheckIn( diff --git a/sentry/src/main/java/io/sentry/ProfileChunk.java b/sentry/src/main/java/io/sentry/ProfileChunk.java new file mode 100644 index 0000000000..44cb921209 --- /dev/null +++ b/sentry/src/main/java/io/sentry/ProfileChunk.java @@ -0,0 +1,317 @@ +package io.sentry; + +import io.sentry.profilemeasurements.ProfileMeasurement; +import io.sentry.protocol.DebugMeta; +import io.sentry.protocol.SdkVersion; +import io.sentry.protocol.SentryId; +import io.sentry.vendor.gson.stream.JsonToken; +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class ProfileChunk implements JsonUnknown, JsonSerializable { + private @Nullable DebugMeta debugMeta; + private @NotNull SentryId profilerId; + private @NotNull SentryId chunkId; + private @Nullable SdkVersion clientSdk; + private final @NotNull Map measurements; + private @NotNull String platform; + private @NotNull String release; + private @Nullable String environment; + private @NotNull String version; + + private final @NotNull File traceFile; + + /** Profile trace encoded with Base64. */ + private @Nullable String sampledProfile = null; + + private @Nullable Map unknown; + + public ProfileChunk() { + this( + SentryId.EMPTY_ID, + SentryId.EMPTY_ID, + new File("dummy"), + new HashMap<>(), + SentryOptions.empty()); + } + + public ProfileChunk( + final @NotNull SentryId profilerId, + final @NotNull SentryId chunkId, + final @NotNull File traceFile, + final @NotNull Map measurements, + final @NotNull SentryOptions options) { + this.profilerId = profilerId; + this.chunkId = chunkId; + this.traceFile = traceFile; + this.measurements = measurements; + this.debugMeta = null; + this.clientSdk = options.getSdkVersion(); + this.release = options.getRelease() != null ? options.getRelease() : ""; + this.environment = options.getEnvironment(); + this.platform = "android"; + this.version = "2"; + } + + public @NotNull Map getMeasurements() { + return measurements; + } + + public @Nullable DebugMeta getDebugMeta() { + return debugMeta; + } + + public void setDebugMeta(final @Nullable DebugMeta debugMeta) { + this.debugMeta = debugMeta; + } + + public @Nullable SdkVersion getClientSdk() { + return clientSdk; + } + + public @NotNull SentryId getChunkId() { + return chunkId; + } + + public @Nullable String getEnvironment() { + return environment; + } + + public @NotNull String getPlatform() { + return platform; + } + + public @NotNull SentryId getProfilerId() { + return profilerId; + } + + public @NotNull String getRelease() { + return release; + } + + public @Nullable String getSampledProfile() { + return sampledProfile; + } + + public void setSampledProfile(final @Nullable String sampledProfile) { + this.sampledProfile = sampledProfile; + } + + public @NotNull File getTraceFile() { + return traceFile; + } + + public @NotNull String getVersion() { + return version; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ProfileChunk)) return false; + ProfileChunk that = (ProfileChunk) o; + return Objects.equals(debugMeta, that.debugMeta) + && Objects.equals(profilerId, that.profilerId) + && Objects.equals(chunkId, that.chunkId) + && Objects.equals(clientSdk, that.clientSdk) + && Objects.equals(measurements, that.measurements) + && Objects.equals(platform, that.platform) + && Objects.equals(release, that.release) + && Objects.equals(environment, that.environment) + && Objects.equals(version, that.version) + && Objects.equals(sampledProfile, that.sampledProfile) + && Objects.equals(unknown, that.unknown); + } + + @Override + public int hashCode() { + return Objects.hash( + debugMeta, + profilerId, + chunkId, + clientSdk, + measurements, + platform, + release, + environment, + version, + sampledProfile, + unknown); + } + + public static final class Builder { + private final @NotNull SentryId profilerId; + private final @NotNull SentryId chunkId; + private final @NotNull Map measurements; + private final @NotNull File traceFile; + + public Builder( + final @NotNull SentryId profilerId, + final @NotNull SentryId chunkId, + final @NotNull Map measurements, + final @NotNull File traceFile) { + this.profilerId = profilerId; + this.chunkId = chunkId; + this.measurements = measurements; + this.traceFile = traceFile; + } + + public ProfileChunk build(SentryOptions options) { + return new ProfileChunk(profilerId, chunkId, traceFile, measurements, options); + } + } + + // JsonSerializable + + public static final class JsonKeys { + public static final String DEBUG_META = "debug_meta"; + public static final String PROFILER_ID = "profiler_id"; + public static final String CHUNK_ID = "chunk_id"; + public static final String CLIENT_SDK = "client_sdk"; + public static final String MEASUREMENTS = "measurements"; + public static final String PLATFORM = "platform"; + public static final String RELEASE = "release"; + public static final String ENVIRONMENT = "environment"; + public static final String VERSION = "version"; + public static final String SAMPLED_PROFILE = "sampled_profile"; + } + + @Override + public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger) + throws IOException { + writer.beginObject(); + if (debugMeta != null) { + writer.name(JsonKeys.DEBUG_META).value(logger, debugMeta); + } + writer.name(JsonKeys.PROFILER_ID).value(logger, profilerId); + writer.name(JsonKeys.CHUNK_ID).value(logger, chunkId); + if (clientSdk != null) { + writer.name(JsonKeys.CLIENT_SDK).value(logger, clientSdk); + } + if (!measurements.isEmpty()) { + writer.name(JsonKeys.MEASUREMENTS).value(logger, measurements); + } + writer.name(JsonKeys.PLATFORM).value(logger, platform); + writer.name(JsonKeys.RELEASE).value(logger, release); + if (environment != null) { + writer.name(JsonKeys.ENVIRONMENT).value(logger, environment); + } + writer.name(JsonKeys.VERSION).value(logger, version); + if (sampledProfile != null) { + writer.name(JsonKeys.SAMPLED_PROFILE).value(logger, sampledProfile); + } + if (unknown != null) { + for (String key : unknown.keySet()) { + Object value = unknown.get(key); + writer.name(key).value(logger, value); + } + } + writer.endObject(); + } + + @Nullable + @Override + public Map getUnknown() { + return unknown; + } + + @Override + public void setUnknown(@Nullable Map unknown) { + this.unknown = unknown; + } + + public static final class Deserializer implements JsonDeserializer { + + @Override + public @NotNull ProfileChunk deserialize(@NotNull ObjectReader reader, @NotNull ILogger logger) + throws Exception { + reader.beginObject(); + ProfileChunk data = new ProfileChunk(); + Map unknown = null; + + while (reader.peek() == JsonToken.NAME) { + final String nextName = reader.nextName(); + switch (nextName) { + case JsonKeys.DEBUG_META: + DebugMeta debugMeta = reader.nextOrNull(logger, new DebugMeta.Deserializer()); + if (debugMeta != null) { + data.debugMeta = debugMeta; + } + break; + case JsonKeys.PROFILER_ID: + SentryId profilerId = reader.nextOrNull(logger, new SentryId.Deserializer()); + if (profilerId != null) { + data.profilerId = profilerId; + } + break; + case JsonKeys.CHUNK_ID: + SentryId chunkId = reader.nextOrNull(logger, new SentryId.Deserializer()); + if (chunkId != null) { + data.chunkId = chunkId; + } + break; + case JsonKeys.CLIENT_SDK: + SdkVersion clientSdk = reader.nextOrNull(logger, new SdkVersion.Deserializer()); + if (clientSdk != null) { + data.clientSdk = clientSdk; + } + break; + case JsonKeys.MEASUREMENTS: + Map measurements = + reader.nextMapOrNull(logger, new ProfileMeasurement.Deserializer()); + if (measurements != null) { + data.measurements.putAll(measurements); + } + break; + case JsonKeys.PLATFORM: + String platform = reader.nextStringOrNull(); + if (platform != null) { + data.platform = platform; + } + break; + case JsonKeys.RELEASE: + String release = reader.nextStringOrNull(); + if (release != null) { + data.release = release; + } + break; + case JsonKeys.ENVIRONMENT: + String environment = reader.nextStringOrNull(); + if (environment != null) { + data.environment = environment; + } + break; + case JsonKeys.VERSION: + String version = reader.nextStringOrNull(); + if (version != null) { + data.version = version; + } + break; + case JsonKeys.SAMPLED_PROFILE: + String sampledProfile = reader.nextStringOrNull(); + if (sampledProfile != null) { + data.sampledProfile = sampledProfile; + } + break; + default: + if (unknown == null) { + unknown = new ConcurrentHashMap<>(); + } + reader.nextUnknown(logger, unknown, nextName); + break; + } + } + data.setUnknown(unknown); + reader.endObject(); + return data; + } + } +} diff --git a/sentry/src/main/java/io/sentry/ProfileContext.java b/sentry/src/main/java/io/sentry/ProfileContext.java new file mode 100644 index 0000000000..3a7fa9af2f --- /dev/null +++ b/sentry/src/main/java/io/sentry/ProfileContext.java @@ -0,0 +1,122 @@ +package io.sentry; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.protocol.SentryId; +import io.sentry.util.CollectionUtils; +import io.sentry.util.Objects; +import io.sentry.vendor.gson.stream.JsonToken; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@Open +public class ProfileContext implements JsonUnknown, JsonSerializable { + public static final String TYPE = "profile"; + + /** Determines which trace the Span belongs to. */ + private @NotNull SentryId profilerId; + + private @Nullable Map unknown; + + public ProfileContext() { + this(SentryId.EMPTY_ID); + } + + public ProfileContext(final @NotNull SentryId profilerId) { + this.profilerId = profilerId; + } + + /** + * Copy constructor. + * + * @param profileContext the ProfileContext to copy + */ + public ProfileContext(final @NotNull ProfileContext profileContext) { + this.profilerId = profileContext.profilerId; + final Map copiedUnknown = + CollectionUtils.newConcurrentHashMap(profileContext.unknown); + if (copiedUnknown != null) { + this.unknown = copiedUnknown; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ProfileContext)) return false; + ProfileContext that = (ProfileContext) o; + return profilerId.equals(that.profilerId); + } + + @Override + public int hashCode() { + return Objects.hash(profilerId); + } + + // region JsonSerializable + + public static final class JsonKeys { + public static final String PROFILER_ID = "profiler_id"; + } + + @Override + public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger) + throws IOException { + writer.beginObject(); + writer.name(JsonKeys.PROFILER_ID).value(logger, profilerId); + if (unknown != null) { + for (String key : unknown.keySet()) { + Object value = unknown.get(key); + writer.name(key).value(logger, value); + } + } + writer.endObject(); + } + + public @NotNull SentryId getProfilerId() { + return profilerId; + } + + @Nullable + @Override + public Map getUnknown() { + return unknown; + } + + @Override + public void setUnknown(@Nullable Map unknown) { + this.unknown = unknown; + } + + public static final class Deserializer implements JsonDeserializer { + @Override + public @NotNull ProfileContext deserialize( + @NotNull ObjectReader reader, @NotNull ILogger logger) throws Exception { + reader.beginObject(); + ProfileContext data = new ProfileContext(); + Map unknown = null; + while (reader.peek() == JsonToken.NAME) { + final String nextName = reader.nextName(); + switch (nextName) { + case JsonKeys.PROFILER_ID: + SentryId profilerId = reader.nextOrNull(logger, new SentryId.Deserializer()); + if (profilerId != null) { + data.profilerId = profilerId; + } + break; + default: + if (unknown == null) { + unknown = new ConcurrentHashMap<>(); + } + reader.nextUnknown(logger, unknown, nextName); + break; + } + } + data.setUnknown(unknown); + reader.endObject(); + return data; + } + } +} diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 6868894340..8a2b1c9bc1 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -10,6 +10,7 @@ import io.sentry.metrics.IMetricsClient; import io.sentry.metrics.NoopMetricsAggregator; import io.sentry.protocol.Contexts; +import io.sentry.protocol.DebugMeta; import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.transport.ITransport; @@ -474,8 +475,7 @@ private SentryEvent processEvent( return event; } - @Nullable - private SentryTransaction processTransaction( + private @Nullable SentryTransaction processTransaction( @NotNull SentryTransaction transaction, final @NotNull Hint hint, final @NotNull List eventProcessors) { @@ -864,6 +864,42 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint return sentryId; } + @ApiStatus.Internal + @Override + public @NotNull SentryId captureProfileChunk( + @NotNull ProfileChunk profileChunk, final @Nullable IScope scope) { + Objects.requireNonNull(profileChunk, "profileChunk is required."); + + options + .getLogger() + .log(SentryLevel.DEBUG, "Capturing profile chunk: %s", profileChunk.getChunkId()); + + @NotNull SentryId sentryId = profileChunk.getChunkId(); + final DebugMeta debugMeta = DebugMeta.buildDebugMeta(profileChunk.getDebugMeta(), options); + if (debugMeta != null) { + profileChunk.setDebugMeta(debugMeta); + } + + // BeforeSend and EventProcessors are not supported at the moment for Profile Chunks + + try { + final @NotNull SentryEnvelope envelope = + new SentryEnvelope( + new SentryEnvelopeHeader(sentryId, options.getSdkVersion(), null), + Collections.singletonList( + SentryEnvelopeItem.fromProfileChunk(profileChunk, options.getSerializer()))); + sentryId = sendEnvelope(envelope, null); + } catch (IOException | SentryEnvelopeException e) { + options + .getLogger() + .log(SentryLevel.WARNING, e, "Capturing profile chunk %s failed.", sentryId); + // if there was an error capturing the event, we return an emptyId + sentryId = SentryId.EMPTY_ID; + } + + return sentryId; + } + @Override @ApiStatus.Experimental public @NotNull SentryId captureCheckIn( diff --git a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java index 7862c8d664..11e397d791 100644 --- a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java +++ b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java @@ -35,6 +35,9 @@ @ApiStatus.Internal public final class SentryEnvelopeItem { + // Profiles bigger than 50 MB will be dropped by the backend, so we drop bigger ones + private static final long MAX_PROFILE_CHUNK_SIZE = 50 * 1024 * 1024; // 50MB + @SuppressWarnings("CharsetObjectCanBeUsed") private static final Charset UTF_8 = Charset.forName("UTF-8"); @@ -278,13 +281,64 @@ private static void ensureAttachmentSizeLimit( } } + public static @NotNull SentryEnvelopeItem fromProfileChunk( + final @NotNull ProfileChunk profileChunk, final @NotNull ISerializer serializer) + throws SentryEnvelopeException { + + final @NotNull File traceFile = profileChunk.getTraceFile(); + // Using CachedItem, so we read the trace file in the background + final CachedItem cachedItem = + new CachedItem( + () -> { + if (!traceFile.exists()) { + throw new SentryEnvelopeException( + String.format( + "Dropping profile chunk, because the file '%s' doesn't exists", + traceFile.getName())); + } + // The payload of the profile item is a json including the trace file encoded with + // base64 + final byte[] traceFileBytes = + readBytesFromFile(traceFile.getPath(), MAX_PROFILE_CHUNK_SIZE); + final @NotNull String base64Trace = + Base64.encodeToString(traceFileBytes, NO_WRAP | NO_PADDING); + if (base64Trace.isEmpty()) { + throw new SentryEnvelopeException("Profiling trace file is empty"); + } + profileChunk.setSampledProfile(base64Trace); + + try (final ByteArrayOutputStream stream = new ByteArrayOutputStream(); + final Writer writer = new BufferedWriter(new OutputStreamWriter(stream, UTF_8))) { + serializer.serialize(profileChunk, writer); + return stream.toByteArray(); + } catch (IOException e) { + throw new SentryEnvelopeException( + String.format("Failed to serialize profile chunk\n%s", e.getMessage())); + } finally { + // In any case we delete the trace file + traceFile.delete(); + } + }); + + SentryEnvelopeItemHeader itemHeader = + new SentryEnvelopeItemHeader( + SentryItemType.ProfileChunk, + () -> cachedItem.getBytes().length, + "application-json", + traceFile.getName()); + + // avoid method refs on Android due to some issues with older AGP setups + // noinspection Convert2MethodRef + return new SentryEnvelopeItem(itemHeader, () -> cachedItem.getBytes()); + } + public static @NotNull SentryEnvelopeItem fromProfilingTrace( final @NotNull ProfilingTraceData profilingTraceData, final long maxTraceFileSize, final @NotNull ISerializer serializer) throws SentryEnvelopeException { - File traceFile = profilingTraceData.getTraceFile(); + final @NotNull File traceFile = profilingTraceData.getTraceFile(); // Using CachedItem, so we read the trace file in the background final CachedItem cachedItem = new CachedItem( @@ -297,8 +351,10 @@ private static void ensureAttachmentSizeLimit( } // The payload of the profile item is a json including the trace file encoded with // base64 - byte[] traceFileBytes = readBytesFromFile(traceFile.getPath(), maxTraceFileSize); - String base64Trace = Base64.encodeToString(traceFileBytes, NO_WRAP | NO_PADDING); + final byte[] traceFileBytes = + readBytesFromFile(traceFile.getPath(), maxTraceFileSize); + final @NotNull String base64Trace = + Base64.encodeToString(traceFileBytes, NO_WRAP | NO_PADDING); if (base64Trace.isEmpty()) { throw new SentryEnvelopeException("Profiling trace file is empty"); } diff --git a/sentry/src/main/java/io/sentry/SentryItemType.java b/sentry/src/main/java/io/sentry/SentryItemType.java index f37b972454..595407036e 100644 --- a/sentry/src/main/java/io/sentry/SentryItemType.java +++ b/sentry/src/main/java/io/sentry/SentryItemType.java @@ -15,6 +15,7 @@ public enum SentryItemType implements JsonSerializable { Attachment("attachment"), Transaction("transaction"), Profile("profile"), + ProfileChunk("profile_chunk"), ClientReport("client_report"), ReplayEvent("replay_event"), ReplayRecording("replay_recording"), diff --git a/sentry/src/main/java/io/sentry/SpanContext.java b/sentry/src/main/java/io/sentry/SpanContext.java index 5a43ff845e..f744068f77 100644 --- a/sentry/src/main/java/io/sentry/SpanContext.java +++ b/sentry/src/main/java/io/sentry/SpanContext.java @@ -104,10 +104,16 @@ public SpanContext(final @NotNull SpanContext spanContext) { this.op = spanContext.op; this.description = spanContext.description; this.status = spanContext.status; + this.origin = spanContext.origin; final Map copiedTags = CollectionUtils.newConcurrentHashMap(spanContext.tags); if (copiedTags != null) { this.tags = copiedTags; } + final Map copiedUnknown = + CollectionUtils.newConcurrentHashMap(spanContext.unknown); + if (copiedUnknown != null) { + this.unknown = copiedUnknown; + } } public void setOperation(final @NotNull String operation) { diff --git a/sentry/src/main/java/io/sentry/profilemeasurements/ProfileMeasurementValue.java b/sentry/src/main/java/io/sentry/profilemeasurements/ProfileMeasurementValue.java index b0cebf5439..12972d36e4 100644 --- a/sentry/src/main/java/io/sentry/profilemeasurements/ProfileMeasurementValue.java +++ b/sentry/src/main/java/io/sentry/profilemeasurements/ProfileMeasurementValue.java @@ -1,14 +1,20 @@ package io.sentry.profilemeasurements; +import io.sentry.DateUtils; import io.sentry.ILogger; import io.sentry.JsonDeserializer; import io.sentry.JsonSerializable; import io.sentry.JsonUnknown; import io.sentry.ObjectReader; import io.sentry.ObjectWriter; +import io.sentry.SentryDate; +import io.sentry.SentryNanotimeDate; import io.sentry.util.Objects; import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Date; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.jetbrains.annotations.ApiStatus; @@ -19,16 +25,26 @@ public final class ProfileMeasurementValue implements JsonUnknown, JsonSerializable { private @Nullable Map unknown; + private @Nullable Double timestamp; private @NotNull String relativeStartNs; // timestamp in nanoseconds this frame was started private double value; // frame duration in nanoseconds + @SuppressWarnings("JavaUtilDate") public ProfileMeasurementValue() { - this(0L, 0); + this(0L, 0, new SentryNanotimeDate(new Date(0), 0)); } - public ProfileMeasurementValue(final @NotNull Long relativeStartNs, final @NotNull Number value) { + public ProfileMeasurementValue( + final @NotNull Long relativeStartNs, + final @NotNull Number value, + final @NotNull SentryDate timestamp) { this.relativeStartNs = relativeStartNs.toString(); this.value = value.doubleValue(); + this.timestamp = DateUtils.nanosToSeconds(timestamp.nanoTimestamp()); + } + + public @Nullable Double getTimestamp() { + return timestamp; } public double getValue() { @@ -46,7 +62,8 @@ public boolean equals(Object o) { ProfileMeasurementValue that = (ProfileMeasurementValue) o; return Objects.equals(unknown, that.unknown) && relativeStartNs.equals(that.relativeStartNs) - && value == that.value; + && value == that.value + && Objects.equals(timestamp, that.timestamp); } @Override @@ -59,6 +76,7 @@ public int hashCode() { public static final class JsonKeys { public static final String VALUE = "value"; public static final String START_NS = "elapsed_since_start_ns"; + public static final String TIMESTAMP = "timestamp"; } @Override @@ -67,6 +85,9 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger writer.beginObject(); writer.name(JsonKeys.VALUE).value(logger, value); writer.name(JsonKeys.START_NS).value(logger, relativeStartNs); + if (timestamp != null) { + writer.name(JsonKeys.TIMESTAMP).value(logger, doubleToBigDecimal(timestamp)); + } if (unknown != null) { for (String key : unknown.keySet()) { Object value = unknown.get(key); @@ -77,6 +98,10 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger writer.endObject(); } + private @NotNull BigDecimal doubleToBigDecimal(final @NotNull Double value) { + return BigDecimal.valueOf(value).setScale(6, RoundingMode.DOWN); + } + @Nullable @Override public Map getUnknown() { @@ -112,6 +137,18 @@ public static final class Deserializer implements JsonDeserializer(); diff --git a/sentry/src/main/java/io/sentry/protocol/Contexts.java b/sentry/src/main/java/io/sentry/protocol/Contexts.java index ba4cbe51cb..166d300d48 100644 --- a/sentry/src/main/java/io/sentry/protocol/Contexts.java +++ b/sentry/src/main/java/io/sentry/protocol/Contexts.java @@ -5,6 +5,7 @@ import io.sentry.JsonSerializable; import io.sentry.ObjectReader; import io.sentry.ObjectWriter; +import io.sentry.ProfileContext; import io.sentry.SpanContext; import io.sentry.util.HintUtils; import io.sentry.util.Objects; @@ -45,6 +46,8 @@ public Contexts(final @NotNull Contexts contexts) { this.setGpu(new Gpu((Gpu) value)); } else if (SpanContext.TYPE.equals(entry.getKey()) && value instanceof SpanContext) { this.setTrace(new SpanContext((SpanContext) value)); + } else if (ProfileContext.TYPE.equals(entry.getKey()) && value instanceof ProfileContext) { + this.setProfile(new ProfileContext((ProfileContext) value)); } else if (Response.TYPE.equals(entry.getKey()) && value instanceof Response) { this.setResponse(new Response((Response) value)); } else { @@ -68,6 +71,15 @@ public void setTrace(final @Nullable SpanContext traceContext) { this.put(SpanContext.TYPE, traceContext); } + public @Nullable ProfileContext getProfile() { + return toContextType(ProfileContext.TYPE, ProfileContext.class); + } + + public void setProfile(final @Nullable ProfileContext profileContext) { + Objects.requireNonNull(profileContext, "profileContext is required"); + this.put(ProfileContext.TYPE, profileContext); + } + public @Nullable App getApp() { return toContextType(App.TYPE, App.class); } @@ -189,6 +201,9 @@ public static final class Deserializer implements JsonDeserializer { case SpanContext.TYPE: contexts.setTrace(new SpanContext.Deserializer().deserialize(reader, logger)); break; + case ProfileContext.TYPE: + contexts.setProfile(new ProfileContext.Deserializer().deserialize(reader, logger)); + break; case Response.TYPE: contexts.setResponse(new Response.Deserializer().deserialize(reader, logger)); break; diff --git a/sentry/src/main/java/io/sentry/protocol/DebugMeta.java b/sentry/src/main/java/io/sentry/protocol/DebugMeta.java index 458c4de631..85ce67ab8e 100644 --- a/sentry/src/main/java/io/sentry/protocol/DebugMeta.java +++ b/sentry/src/main/java/io/sentry/protocol/DebugMeta.java @@ -6,12 +6,14 @@ import io.sentry.JsonUnknown; import io.sentry.ObjectReader; import io.sentry.ObjectWriter; +import io.sentry.SentryOptions; import io.sentry.vendor.gson.stream.JsonToken; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -51,6 +53,42 @@ public void setSdkInfo(final @Nullable SdkInfo sdkInfo) { this.sdkInfo = sdkInfo; } + @ApiStatus.Internal + public static @Nullable DebugMeta buildDebugMeta( + final @Nullable DebugMeta eventDebugMeta, final @NotNull SentryOptions options) { + final @NotNull List debugImages = new ArrayList<>(); + + if (options.getProguardUuid() != null) { + final DebugImage proguardMappingImage = new DebugImage(); + proguardMappingImage.setType(DebugImage.PROGUARD); + proguardMappingImage.setUuid(options.getProguardUuid()); + debugImages.add(proguardMappingImage); + } + + for (final @NotNull String bundleId : options.getBundleIds()) { + final DebugImage sourceBundleImage = new DebugImage(); + sourceBundleImage.setType(DebugImage.JVM); + sourceBundleImage.setDebugId(bundleId); + debugImages.add(sourceBundleImage); + } + + if (!debugImages.isEmpty()) { + DebugMeta debugMeta = eventDebugMeta; + + if (debugMeta == null) { + debugMeta = new DebugMeta(); + } + if (debugMeta.getImages() == null) { + debugMeta.setImages(debugImages); + } else { + debugMeta.getImages().addAll(debugImages); + } + + return debugMeta; + } + return null; + } + // JsonKeys public static final class JsonKeys { diff --git a/sentry/src/test/java/io/sentry/HubAdapterTest.kt b/sentry/src/test/java/io/sentry/HubAdapterTest.kt index 9686250d20..f9f17b3c85 100644 --- a/sentry/src/test/java/io/sentry/HubAdapterTest.kt +++ b/sentry/src/test/java/io/sentry/HubAdapterTest.kt @@ -213,6 +213,12 @@ class HubAdapterTest { verify(hub).captureTransaction(eq(transaction), eq(traceContext), eq(hint), eq(profilingTraceData)) } + @Test fun `captureProfileChunk calls Hub`() { + val profileChunk = mock() + HubAdapter.getInstance().captureProfileChunk(profileChunk) + verify(hub).captureProfileChunk(eq(profileChunk)) + } + @Test fun `startTransaction calls Hub`() { val transactionContext = mock() val samplingContext = mock() diff --git a/sentry/src/test/java/io/sentry/HubTest.kt b/sentry/src/test/java/io/sentry/HubTest.kt index 50f996ccdd..c432729870 100644 --- a/sentry/src/test/java/io/sentry/HubTest.kt +++ b/sentry/src/test/java/io/sentry/HubTest.kt @@ -1491,6 +1491,52 @@ class HubTest { } //endregion + //region captureProfileChunk tests + @Test + fun `when captureProfileChunk is called on disabled client, do nothing`() { + val options = SentryOptions() + options.cacheDirPath = file.absolutePath + options.dsn = "https://key@sentry.io/proj" + options.setSerializer(mock()) + val sut = Hub(options) + val mockClient = mock() + sut.bindClient(mockClient) + sut.close() + + sut.captureProfileChunk(mock()) + verify(mockClient, never()).captureProfileChunk(any(), any()) + verify(mockClient, never()).captureProfileChunk(any(), any()) + } + + @Test + fun `when captureProfileChunk, captureProfileChunk on the client should be called`() { + val options = SentryOptions() + options.cacheDirPath = file.absolutePath + options.dsn = "https://key@sentry.io/proj" + options.setSerializer(mock()) + val sut = Hub(options) + val mockClient = mock() + sut.bindClient(mockClient) + + val profileChunk = mock() + sut.captureProfileChunk(profileChunk) + verify(mockClient).captureProfileChunk(eq(profileChunk), any()) + } + + @Test + fun `when profileChunk is called, lastEventId is not set`() { + val options = SentryOptions().apply { + dsn = "https://key@sentry.io/proj" + setSerializer(mock()) + } + val sut = Hub(options) + val mockClient = mock() + sut.bindClient(mockClient) + sut.captureProfileChunk(mock()) + assertEquals(SentryId.EMPTY_ID, sut.lastEventId) + } + //endregion + //region profiling tests @Test diff --git a/sentry/src/test/java/io/sentry/JavaMemoryCollectorTest.kt b/sentry/src/test/java/io/sentry/JavaMemoryCollectorTest.kt index e2914edff6..5a41c8facd 100644 --- a/sentry/src/test/java/io/sentry/JavaMemoryCollectorTest.kt +++ b/sentry/src/test/java/io/sentry/JavaMemoryCollectorTest.kt @@ -24,5 +24,6 @@ class JavaMemoryCollectorTest { assertEquals(-1, memoryData.usedNativeMemory) assertEquals(usedMemory, memoryData.usedHeapMemory) assertNotEquals(0, memoryData.timestampMillis) + assertNotEquals(0, memoryData.timestamp.nanoTimestamp()) } } diff --git a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt index ba8ee84d51..4ea1fa24da 100644 --- a/sentry/src/test/java/io/sentry/JsonSerializerTest.kt +++ b/sentry/src/test/java/io/sentry/JsonSerializerTest.kt @@ -28,8 +28,11 @@ import java.io.OutputStream import java.io.OutputStreamWriter import java.io.StringReader import java.io.StringWriter +import java.math.BigDecimal +import java.math.RoundingMode import java.nio.file.Files import java.util.Date +import java.util.HashMap import java.util.TimeZone import java.util.UUID import kotlin.test.BeforeTest @@ -491,10 +494,29 @@ class JsonSerializerTest { } } + @Test + fun `serializes profile context`() { + val profileContext = ProfileContext(SentryId("3367f5196c494acaae85bbbd535379ac")) + val expected = """{"profiler_id":"3367f5196c494acaae85bbbd535379ac"}""" + val json = serializeToString(profileContext) + assertEquals(expected, json) + } + + @Test + fun `deserializes profile context`() { + val json = """{"profiler_id":"3367f5196c494acaae85bbbd535379ac"}""" + val actual = fixture.serializer.deserialize(StringReader(json), ProfileContext::class.java) + assertNotNull(actual) { + assertEquals(SentryId("3367f5196c494acaae85bbbd535379ac"), it.profilerId) + } + } + @Test fun `serializes profilingTraceData`() { val profilingTraceData = ProfilingTraceData(fixture.traceFile, NoOpTransaction.getInstance()) val now = Date() + val measurementNow = SentryNanotimeDate() + val measurementNowSeconds = BigDecimal.valueOf(DateUtils.nanosToSeconds(measurementNow.nanoTimestamp())).setScale(6, RoundingMode.DOWN).toDouble() profilingTraceData.androidApiLevel = 21 profilingTraceData.deviceLocale = "deviceLocale" profilingTraceData.deviceManufacturer = "deviceManufacturer" @@ -524,22 +546,22 @@ class JsonSerializerTest { ProfileMeasurement.ID_SCREEN_FRAME_RATES to ProfileMeasurement( ProfileMeasurement.UNIT_HZ, - listOf(ProfileMeasurementValue(1, 60.1)) + listOf(ProfileMeasurementValue(1, 60.1, measurementNow)) ), ProfileMeasurement.ID_MEMORY_FOOTPRINT to ProfileMeasurement( ProfileMeasurement.UNIT_BYTES, - listOf(ProfileMeasurementValue(2, 100.52)) + listOf(ProfileMeasurementValue(2, 100.52, measurementNow)) ), ProfileMeasurement.ID_MEMORY_NATIVE_FOOTPRINT to ProfileMeasurement( ProfileMeasurement.UNIT_BYTES, - listOf(ProfileMeasurementValue(3, 104.52)) + listOf(ProfileMeasurementValue(3, 104.52, measurementNow)) ), ProfileMeasurement.ID_CPU_USAGE to ProfileMeasurement( ProfileMeasurement.UNIT_PERCENT, - listOf(ProfileMeasurementValue(5, 10.52)) + listOf(ProfileMeasurementValue(5, 10.52, measurementNow)) ) ) ) @@ -595,7 +617,8 @@ class JsonSerializerTest { "values" to listOf( mapOf( "value" to 60.1, - "elapsed_since_start_ns" to "1" + "elapsed_since_start_ns" to "1", + "timestamp" to measurementNowSeconds ) ) ), @@ -605,7 +628,8 @@ class JsonSerializerTest { "values" to listOf( mapOf( "value" to 100.52, - "elapsed_since_start_ns" to "2" + "elapsed_since_start_ns" to "2", + "timestamp" to measurementNowSeconds ) ) ), @@ -615,7 +639,8 @@ class JsonSerializerTest { "values" to listOf( mapOf( "value" to 104.52, - "elapsed_since_start_ns" to "3" + "elapsed_since_start_ns" to "3", + "timestamp" to measurementNowSeconds ) ) ), @@ -625,7 +650,8 @@ class JsonSerializerTest { "values" to listOf( mapOf( "value" to 10.52, - "elapsed_since_start_ns" to "5" + "elapsed_since_start_ns" to "5", + "timestamp" to measurementNowSeconds ) ) ) @@ -756,23 +782,23 @@ class JsonSerializerTest { val expectedMeasurements = mapOf( ProfileMeasurement.ID_SCREEN_FRAME_RATES to ProfileMeasurement( ProfileMeasurement.UNIT_HZ, - listOf(ProfileMeasurementValue(1, 60.1)) + listOf(ProfileMeasurementValue(1, 60.1, mock())) ), ProfileMeasurement.ID_FROZEN_FRAME_RENDERS to ProfileMeasurement( ProfileMeasurement.UNIT_NANOSECONDS, - listOf(ProfileMeasurementValue(2, 100)) + listOf(ProfileMeasurementValue(2, 100, mock())) ), ProfileMeasurement.ID_MEMORY_FOOTPRINT to ProfileMeasurement( ProfileMeasurement.UNIT_BYTES, - listOf(ProfileMeasurementValue(3, 1000)) + listOf(ProfileMeasurementValue(3, 1000, mock())) ), ProfileMeasurement.ID_MEMORY_NATIVE_FOOTPRINT to ProfileMeasurement( ProfileMeasurement.UNIT_BYTES, - listOf(ProfileMeasurementValue(4, 1100)) + listOf(ProfileMeasurementValue(4, 1100, mock())) ), ProfileMeasurement.ID_CPU_USAGE to ProfileMeasurement( ProfileMeasurement.UNIT_PERCENT, - listOf(ProfileMeasurementValue(5, 17.04)) + listOf(ProfileMeasurementValue(5, 17.04, mock())) ) ) assertEquals(expectedMeasurements, profilingTraceData.measurementsMap) @@ -789,10 +815,11 @@ class JsonSerializerTest { @Test fun `serializes profileMeasurement`() { - val measurementValues = listOf(ProfileMeasurementValue(1, 2), ProfileMeasurementValue(3, 4)) + val now = SentryNanotimeDate(Date(1), 1) + val measurementValues = listOf(ProfileMeasurementValue(1, 2, now), ProfileMeasurementValue(3, 4, now)) val profileMeasurement = ProfileMeasurement(ProfileMeasurement.UNIT_NANOSECONDS, measurementValues) val actual = serializeToString(profileMeasurement) - val expected = "{\"unit\":\"nanosecond\",\"values\":[{\"value\":2.0,\"elapsed_since_start_ns\":\"1\"},{\"value\":4.0,\"elapsed_since_start_ns\":\"3\"}]}" + val expected = "{\"unit\":\"nanosecond\",\"values\":[{\"value\":2.0,\"elapsed_since_start_ns\":\"1\",\"timestamp\":0.001000},{\"value\":4.0,\"elapsed_since_start_ns\":\"3\",\"timestamp\":0.001000}]}" assertEquals(expected, actual) } @@ -801,22 +828,22 @@ class JsonSerializerTest { val json = """{ "unit":"hz", "values":[ - {"value":"60.1","elapsed_since_start_ns":"1"},{"value":"100","elapsed_since_start_ns":"2"} + {"value":"60.1","elapsed_since_start_ns":"1"},{"value":"100","elapsed_since_start_ns":"2", "timestamp": 0.001} ] }""" val profileMeasurement = fixture.serializer.deserialize(StringReader(json), ProfileMeasurement::class.java) val expected = ProfileMeasurement( ProfileMeasurement.UNIT_HZ, - listOf(ProfileMeasurementValue(1, 60.1), ProfileMeasurementValue(2, 100)) + listOf(ProfileMeasurementValue(1, 60.1, SentryNanotimeDate(Date(0), 0)), ProfileMeasurementValue(2, 100, SentryNanotimeDate(Date(1), 1))) ) assertEquals(expected, profileMeasurement) } @Test fun `serializes profileMeasurementValue`() { - val profileMeasurementValue = ProfileMeasurementValue(1, 2) + val profileMeasurementValue = ProfileMeasurementValue(1, 2, SentryNanotimeDate(Date(1), 1)) val actual = serializeToString(profileMeasurementValue) - val expected = "{\"value\":2.0,\"elapsed_since_start_ns\":\"1\"}" + val expected = "{\"value\":2.0,\"elapsed_since_start_ns\":\"1\",\"timestamp\":0.001000}" assertEquals(expected, actual) } @@ -824,10 +851,205 @@ class JsonSerializerTest { fun `deserializes profileMeasurementValue`() { val json = """{"value":"60.1","elapsed_since_start_ns":"1"}""" val profileMeasurementValue = fixture.serializer.deserialize(StringReader(json), ProfileMeasurementValue::class.java) - val expected = ProfileMeasurementValue(1, 60.1) + val expected = ProfileMeasurementValue(1, 60.1, mock()) assertEquals(expected, profileMeasurementValue) assertEquals(60.1, profileMeasurementValue?.value) assertEquals("1", profileMeasurementValue?.relativeStartNs) + assertEquals(0.0, profileMeasurementValue?.timestamp) + } + + @Test + fun `deserializes profileMeasurementValue with timestamp`() { + val json = """{"value":"60.1","elapsed_since_start_ns":"1","timestamp":0.001000}""" + val profileMeasurementValue = fixture.serializer.deserialize(StringReader(json), ProfileMeasurementValue::class.java) + val expected = ProfileMeasurementValue(1, 60.1, SentryNanotimeDate(Date(1), 1)) + assertEquals(expected, profileMeasurementValue) + assertEquals(60.1, profileMeasurementValue?.value) + assertEquals("1", profileMeasurementValue?.relativeStartNs) + assertEquals(0.001, profileMeasurementValue?.timestamp) + } + + @Test + fun `serializes profileChunk`() { + val profilerId = SentryId() + val chunkId = SentryId() + fixture.options.sdkVersion = SdkVersion("test", "1.2.3") + fixture.options.release = "release" + fixture.options.environment = "environment" + val profileChunk = ProfileChunk(profilerId, chunkId, fixture.traceFile, HashMap(), fixture.options) + val measurementNow = SentryNanotimeDate() + val measurementNowSeconds = + BigDecimal.valueOf(DateUtils.nanosToSeconds(measurementNow.nanoTimestamp())).setScale(6, RoundingMode.DOWN) + .toDouble() + profileChunk.sampledProfile = "sampled profile in base 64" + profileChunk.measurements.putAll( + hashMapOf( + ProfileMeasurement.ID_SCREEN_FRAME_RATES to + ProfileMeasurement( + ProfileMeasurement.UNIT_HZ, + listOf(ProfileMeasurementValue(1, 60.1, measurementNow)) + ), + ProfileMeasurement.ID_MEMORY_FOOTPRINT to + ProfileMeasurement( + ProfileMeasurement.UNIT_BYTES, + listOf(ProfileMeasurementValue(2, 100.52, measurementNow)) + ), + ProfileMeasurement.ID_MEMORY_NATIVE_FOOTPRINT to + ProfileMeasurement( + ProfileMeasurement.UNIT_BYTES, + listOf(ProfileMeasurementValue(3, 104.52, measurementNow)) + ), + ProfileMeasurement.ID_CPU_USAGE to + ProfileMeasurement( + ProfileMeasurement.UNIT_PERCENT, + listOf(ProfileMeasurementValue(5, 10.52, measurementNow)) + ) + ) + ) + + val actual = serializeToString(profileChunk) + val reader = StringReader(actual) + val objectReader = JsonObjectReader(reader) + val element = JsonObjectDeserializer().deserialize(objectReader) as Map<*, *> + + assertEquals("android", element["platform"] as String) + assertEquals(profilerId.toString(), element["profiler_id"] as String) + assertEquals(chunkId.toString(), element["chunk_id"] as String) + assertEquals("environment", element["environment"] as String) + assertEquals("release", element["release"] as String) + assertEquals(mapOf("name" to "test", "version" to "1.2.3"), element["client_sdk"] as Map) + assertEquals("2", element["version"] as String) + assertEquals("sampled profile in base 64", element["sampled_profile"] as String) + assertEquals( + mapOf( + ProfileMeasurement.ID_SCREEN_FRAME_RATES to + mapOf( + "unit" to ProfileMeasurement.UNIT_HZ, + "values" to listOf( + mapOf( + "value" to 60.1, + "elapsed_since_start_ns" to "1", + "timestamp" to measurementNowSeconds + ) + ) + ), + ProfileMeasurement.ID_MEMORY_FOOTPRINT to + mapOf( + "unit" to ProfileMeasurement.UNIT_BYTES, + "values" to listOf( + mapOf( + "value" to 100.52, + "elapsed_since_start_ns" to "2", + "timestamp" to measurementNowSeconds + ) + ) + ), + ProfileMeasurement.ID_MEMORY_NATIVE_FOOTPRINT to + mapOf( + "unit" to ProfileMeasurement.UNIT_BYTES, + "values" to listOf( + mapOf( + "value" to 104.52, + "elapsed_since_start_ns" to "3", + "timestamp" to measurementNowSeconds + ) + ) + ), + ProfileMeasurement.ID_CPU_USAGE to + mapOf( + "unit" to ProfileMeasurement.UNIT_PERCENT, + "values" to listOf( + mapOf( + "value" to 10.52, + "elapsed_since_start_ns" to "5", + "timestamp" to measurementNowSeconds + ) + ) + ) + ), + element["measurements"] + ) + } + + @Test + fun `deserializes profileChunk`() { + val profilerId = SentryId() + val chunkId = SentryId() + val json = """{ + "client_sdk":{"name":"test","version":"1.2.3"}, + "chunk_id":"$chunkId", + "environment":"environment", + "platform":"android", + "profiler_id":"$profilerId", + "release":"release", + "sampled_profile":"sampled profile in base 64", + "version":"2", + "measurements":{ + "screen_frame_rates": { + "unit":"hz", + "values":[ + {"value":"60.1","elapsed_since_start_ns":"1"} + ] + }, + "frozen_frame_renders": { + "unit":"nanosecond", + "values":[ + {"value":"100","elapsed_since_start_ns":"2"} + ] + }, + "memory_footprint": { + "unit":"byte", + "values":[ + {"value":"1000","elapsed_since_start_ns":"3"} + ] + }, + "memory_native_footprint": { + "unit":"byte", + "values":[ + {"value":"1100","elapsed_since_start_ns":"4"} + ] + }, + "cpu_usage": { + "unit":"percent", + "values":[ + {"value":"17.04","elapsed_since_start_ns":"5"} + ] + } + } + }""" + val profileChunk = fixture.serializer.deserialize(StringReader(json), ProfileChunk::class.java) + assertNotNull(profileChunk) + assertEquals(SdkVersion("test", "1.2.3"), profileChunk.clientSdk) + assertEquals(chunkId, profileChunk.chunkId) + assertEquals("environment", profileChunk.environment) + assertEquals("android", profileChunk.platform) + assertEquals(profilerId, profileChunk.profilerId) + assertEquals("release", profileChunk.release) + assertEquals("sampled profile in base 64", profileChunk.sampledProfile) + assertEquals("2", profileChunk.version) + val expectedMeasurements = mapOf( + ProfileMeasurement.ID_SCREEN_FRAME_RATES to ProfileMeasurement( + ProfileMeasurement.UNIT_HZ, + listOf(ProfileMeasurementValue(1, 60.1, mock())) + ), + ProfileMeasurement.ID_FROZEN_FRAME_RENDERS to ProfileMeasurement( + ProfileMeasurement.UNIT_NANOSECONDS, + listOf(ProfileMeasurementValue(2, 100, mock())) + ), + ProfileMeasurement.ID_MEMORY_FOOTPRINT to ProfileMeasurement( + ProfileMeasurement.UNIT_BYTES, + listOf(ProfileMeasurementValue(3, 1000, mock())) + ), + ProfileMeasurement.ID_MEMORY_NATIVE_FOOTPRINT to ProfileMeasurement( + ProfileMeasurement.UNIT_BYTES, + listOf(ProfileMeasurementValue(4, 1100, mock())) + ), + ProfileMeasurement.ID_CPU_USAGE to ProfileMeasurement( + ProfileMeasurement.UNIT_PERCENT, + listOf(ProfileMeasurementValue(5, 17.04, mock())) + ) + ) + assertEquals(expectedMeasurements, profileChunk.measurements) } @Test diff --git a/sentry/src/test/java/io/sentry/NoOpHubTest.kt b/sentry/src/test/java/io/sentry/NoOpHubTest.kt index dbbfb4b4f1..4f097b2828 100644 --- a/sentry/src/test/java/io/sentry/NoOpHubTest.kt +++ b/sentry/src/test/java/io/sentry/NoOpHubTest.kt @@ -32,6 +32,10 @@ class NoOpHubTest { fun `captureTransaction returns empty SentryId`() = assertEquals(SentryId.EMPTY_ID, sut.captureTransaction(mock(), mock())) + @Test + fun `captureProfileChunk returns empty SentryId`() = + assertEquals(SentryId.EMPTY_ID, sut.captureProfileChunk(mock())) + @Test fun `captureException returns empty SentryId`() = assertEquals(SentryId.EMPTY_ID, sut.captureException(RuntimeException())) diff --git a/sentry/src/test/java/io/sentry/NoOpSentryClientTest.kt b/sentry/src/test/java/io/sentry/NoOpSentryClientTest.kt index 919ce5f083..8f8d76eba2 100644 --- a/sentry/src/test/java/io/sentry/NoOpSentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/NoOpSentryClientTest.kt @@ -63,6 +63,10 @@ class NoOpSentryClientTest { fun `captureTransaction returns empty SentryId`() = assertEquals(SentryId.EMPTY_ID, sut.captureTransaction(mock(), mock())) + @Test + fun `captureProfileChunk returns empty SentryId`() = + assertEquals(SentryId.EMPTY_ID, sut.captureProfileChunk(mock(), mock())) + @Test fun `captureCheckIn returns empty id`() { assertEquals(SentryId.EMPTY_ID, sut.captureCheckIn(mock(), mock(), mock())) diff --git a/sentry/src/test/java/io/sentry/PerformanceCollectionDataTest.kt b/sentry/src/test/java/io/sentry/PerformanceCollectionDataTest.kt index e105e105c6..ad821b952c 100644 --- a/sentry/src/test/java/io/sentry/PerformanceCollectionDataTest.kt +++ b/sentry/src/test/java/io/sentry/PerformanceCollectionDataTest.kt @@ -1,5 +1,6 @@ package io.sentry +import org.mockito.kotlin.mock import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotEquals @@ -16,8 +17,10 @@ class PerformanceCollectionDataTest { @Test fun `only the last of multiple memory data is saved`() { val data = fixture.getSut() - val memData1 = MemoryCollectionData(0, 0, 0) - val memData2 = MemoryCollectionData(1, 1, 1) + val t1 = mock() + val t2 = mock() + val memData1 = MemoryCollectionData(0, 0, 0, t1) + val memData2 = MemoryCollectionData(1, 1, 1, t2) data.addMemoryData(memData1) data.addMemoryData(memData2) val savedMemoryData = data.memoryData @@ -28,8 +31,10 @@ class PerformanceCollectionDataTest { @Test fun `only the last of multiple cpu data is saved`() { val data = fixture.getSut() - val cpuData1 = CpuCollectionData(0, 0.0) - val cpuData2 = CpuCollectionData(1, 1.0) + val t1 = mock() + val t2 = mock() + val cpuData1 = CpuCollectionData(0, 0.0, t1) + val cpuData2 = CpuCollectionData(1, 1.0, t2) data.addCpuData(cpuData1) data.addCpuData(cpuData2) val savedCpuData = data.cpuData @@ -40,7 +45,7 @@ class PerformanceCollectionDataTest { @Test fun `null values are ignored`() { val data = fixture.getSut() - val cpuData1 = CpuCollectionData(0, 0.0) + val cpuData1 = CpuCollectionData(0, 0.0, mock()) data.addCpuData(cpuData1) data.addCpuData(null) data.addMemoryData(null) diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index f87a148bf1..ccee0d26c2 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -42,6 +42,7 @@ import org.mockito.kotlin.mockingDetails import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.verifyNoMoreInteractions import org.mockito.kotlin.whenever import org.msgpack.core.MessagePack @@ -78,6 +79,8 @@ class SentryClientTest { val maxAttachmentSize: Long = (5 * 1024 * 1024).toLong() val hub = mock() val sentryTracer: SentryTracer + val profileChunk: ProfileChunk + val profilingTraceFile = Files.createTempFile("trace", ".trace").toFile() var sentryOptions: SentryOptions = SentryOptions().apply { dsn = dsnString @@ -97,12 +100,12 @@ class SentryClientTest { whenever(hub.options).thenReturn(sentryOptions) sentryTracer = SentryTracer(TransactionContext("a-transaction", "op", TracesSamplingDecision(true)), hub) sentryTracer.startChild("a-span", "span 1").finish() + profileChunk = ProfileChunk(SentryId(), SentryId(), profilingTraceFile, emptyMap(), sentryOptions) } var attachment = Attachment("hello".toByteArray(), "hello.txt", "text/plain", true) var attachment2 = Attachment("hello2".toByteArray(), "hello2.txt", "text/plain", true) var attachment3 = Attachment("hello3".toByteArray(), "hello3.txt", "text/plain", true) - val profilingTraceFile = Files.createTempFile("trace", ".trace").toFile() var profilingTraceData = ProfilingTraceData(profilingTraceFile, sentryTracer) var profilingNonExistingTraceData = ProfilingTraceData(File("non_existent.trace"), sentryTracer) @@ -1085,6 +1088,22 @@ class SentryClientTest { ) } + @Test + fun `captureProfileChunk ignores beforeSend`() { + var invoked = false + fixture.sentryOptions.setBeforeSendTransaction { t, _ -> invoked = true; t } + fixture.getSut().captureProfileChunk(fixture.profileChunk, mock()) + assertFalse(invoked) + } + + @Test + fun `captureProfileChunk ignores Event Processors`() { + val mockProcessor = mock() + fixture.sentryOptions.addEventProcessor(mockProcessor) + fixture.getSut().captureProfileChunk(fixture.profileChunk, mock()) + verifyNoInteractions(mockProcessor) + } + @Test fun `when captureSession and no release is set, do nothing`() { fixture.getSut().captureSession(createSession("")) @@ -1485,6 +1504,29 @@ class SentryClientTest { assertFails { verifyProfilingTraceInEnvelope(SentryId(fixture.profilingNonExistingTraceData.profileId)) } } + @Test + fun `when captureProfileChunk`() { + val client = fixture.getSut() + client.captureProfileChunk(fixture.profileChunk, mock()) + verifyProfileChunkInEnvelope(fixture.profileChunk.chunkId) + } + + @Test + fun `when captureProfileChunk with empty trace file, profile chunk is not sent`() { + val client = fixture.getSut() + fixture.profilingTraceFile.writeText("") + client.captureProfileChunk(fixture.profileChunk, mock()) + assertFails { verifyProfilingTraceInEnvelope(fixture.profileChunk.chunkId) } + } + + @Test + fun `when captureProfileChunk with non existing profiling trace file, profile chunk is not sent`() { + val client = fixture.getSut() + fixture.profilingTraceFile.delete() + client.captureProfileChunk(fixture.profileChunk, mock()) + assertFails { verifyProfilingTraceInEnvelope(fixture.profileChunk.chunkId) } + } + @Test fun `when captureTransaction with attachments not added to transaction`() { val transaction = SentryTransaction(fixture.sentryTracer) @@ -3025,6 +3067,19 @@ class SentryClientTest { ) } + private fun verifyProfileChunkInEnvelope(eventId: SentryId?) { + verify(fixture.transport).send( + check { actual -> + assertEquals(eventId, actual.header.eventId) + + val profilingTraceItem = actual.items.firstOrNull { item -> + item.header.type == SentryItemType.ProfileChunk + } + assertNotNull(profilingTraceItem?.data) + } + ) + } + private class AbnormalHint(private val mechanism: String? = null) : AbnormalExit { override fun mechanism(): String? = mechanism override fun ignoreCurrentThread(): Boolean = false diff --git a/sentry/src/test/java/io/sentry/SentryEnvelopeItemTest.kt b/sentry/src/test/java/io/sentry/SentryEnvelopeItemTest.kt index 760d1270e5..a85e940e22 100644 --- a/sentry/src/test/java/io/sentry/SentryEnvelopeItemTest.kt +++ b/sentry/src/test/java/io/sentry/SentryEnvelopeItemTest.kt @@ -462,6 +462,94 @@ class SentryEnvelopeItemTest { ) } + @Test + fun `fromProfileChunk saves file as Base64`() { + val file = File(fixture.pathname) + val profileChunk = mock { + whenever(it.traceFile).thenReturn(file) + } + + file.writeBytes(fixture.bytes) + val chunk = SentryEnvelopeItem.fromProfileChunk(profileChunk, mock()).data + verify(profileChunk).sampledProfile = + Base64.encodeToString(fixture.bytes, Base64.NO_WRAP or Base64.NO_PADDING) + } + + @Test + fun `fromProfileChunk deletes file only after reading data`() { + val file = File(fixture.pathname) + val profileChunk = mock { + whenever(it.traceFile).thenReturn(file) + } + + file.writeBytes(fixture.bytes) + assert(file.exists()) + val chunk = SentryEnvelopeItem.fromProfileChunk(profileChunk, mock()) + assert(file.exists()) + chunk.data + assertFalse(file.exists()) + } + + @Test + fun `fromProfileChunk with invalid file throws`() { + val file = File(fixture.pathname) + val profileChunk = mock { + whenever(it.traceFile).thenReturn(file) + } + + assertFailsWith("Dropping profiling trace data, because the file ${file.path} doesn't exists") { + SentryEnvelopeItem.fromProfileChunk(profileChunk, mock()).data + } + } + + @Test + fun `fromProfileChunk with unreadable file throws`() { + val file = File(fixture.pathname) + val profileChunk = mock { + whenever(it.traceFile).thenReturn(file) + } + file.writeBytes(fixture.bytes) + file.setReadable(false) + assertFailsWith("Dropping profiling trace data, because the file ${file.path} doesn't exists") { + SentryEnvelopeItem.fromProfileChunk(profileChunk, mock()).data + } + } + + @Test + fun `fromProfileChunk with empty file throws`() { + val file = File(fixture.pathname) + file.writeBytes(ByteArray(0)) + val profileChunk = mock { + whenever(it.traceFile).thenReturn(file) + } + + val chunk = SentryEnvelopeItem.fromProfileChunk(profileChunk, mock()) + assertFailsWith("Profiling trace file is empty") { + chunk.data + } + } + + @Test + fun `fromProfileChunk with file too big`() { + val file = File(fixture.pathname) + val maxSize = 50 * 1024 * 1024 // 50MB + file.writeBytes(ByteArray((maxSize + 1)) { 0 }) + val profileChunk = mock { + whenever(it.traceFile).thenReturn(file) + } + + val exception = assertFailsWith { + SentryEnvelopeItem.fromProfileChunk(profileChunk, mock()).data + } + + assertEquals( + "Reading file failed, because size located at " + + "'${fixture.pathname}' with ${file.length()} bytes is bigger than the maximum " + + "allowed size of $maxSize bytes.", + exception.message + ) + } + @Test fun `fromReplay encodes payload into msgpack`() { val file = Files.createTempFile("replay", "").toFile() diff --git a/sentry/src/test/java/io/sentry/protocol/ContextsTest.kt b/sentry/src/test/java/io/sentry/protocol/ContextsTest.kt index c1fb47b1c7..88974c9556 100644 --- a/sentry/src/test/java/io/sentry/protocol/ContextsTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/ContextsTest.kt @@ -1,5 +1,6 @@ package io.sentry.protocol +import io.sentry.ProfileContext import io.sentry.SpanContext import kotlin.test.Test import kotlin.test.assertEquals @@ -19,6 +20,7 @@ class ContextsTest { contexts.setGpu(Gpu()) contexts.setResponse(Response()) contexts.trace = SpanContext("op") + contexts.profile = ProfileContext(SentryId()) val clone = Contexts(contexts) @@ -31,15 +33,18 @@ class ContextsTest { assertNotSame(contexts.runtime, clone.runtime) assertNotSame(contexts.gpu, clone.gpu) assertNotSame(contexts.trace, clone.trace) + assertNotSame(contexts.profile, clone.profile) assertNotSame(contexts.response, clone.response) } @Test fun `copying contexts will have the same values`() { val contexts = Contexts() + val id = SentryId() contexts["some-property"] = "some-value" contexts.trace = SpanContext("op") contexts.trace!!.description = "desc" + contexts.profile = ProfileContext(id) val clone = Contexts(contexts) @@ -47,5 +52,6 @@ class ContextsTest { assertNotSame(contexts, clone) assertEquals(contexts["some-property"], clone["some-property"]) assertEquals(contexts.trace!!.description, clone.trace!!.description) + assertEquals(contexts.profile!!.profilerId, clone.profile!!.profilerId) } } diff --git a/sentry/src/test/java/io/sentry/protocol/DebugMetaTest.kt b/sentry/src/test/java/io/sentry/protocol/DebugMetaTest.kt index 17544a300f..21395dfc5c 100644 --- a/sentry/src/test/java/io/sentry/protocol/DebugMetaTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/DebugMetaTest.kt @@ -1,5 +1,6 @@ package io.sentry.protocol +import io.sentry.SentryOptions import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -16,4 +17,75 @@ class DebugMetaTest { assertEquals(3, it.size) } } + + @Test + fun `when event does not have debug meta and proguard uuids are set, attaches debug information`() { + val options = SentryOptions().apply { proguardUuid = "id1" } + val debugMeta = DebugMeta.buildDebugMeta(null, options) + + assertNotNull(debugMeta) { + assertNotNull(it.images) { images -> + assertEquals("id1", images[0].uuid) + assertEquals("proguard", images[0].type) + } + } + } + + @Test + fun `when event does not have debug meta and bundle ids are set, attaches debug information`() { + val options = SentryOptions().apply { bundleIds.addAll(listOf("id1", "id2")) } + val debugMeta = DebugMeta.buildDebugMeta(null, options) + + assertNotNull(debugMeta) { + assertNotNull(it.images) { images -> + assertEquals("id1", images[0].debugId) + assertEquals("jvm", images[0].type) + assertEquals("id2", images[1].debugId) + assertEquals("jvm", images[1].type) + } + } + } + + @Test + fun `when event has debug meta and proguard uuids are set, attaches debug information`() { + val options = SentryOptions().apply { proguardUuid = "id1" } + val debugMeta = DebugMeta.buildDebugMeta(DebugMeta(), options) + + assertNotNull(debugMeta) { + assertNotNull(it.images) { images -> + assertEquals("id1", images[0].uuid) + assertEquals("proguard", images[0].type) + } + } + } + + @Test + fun `when event has debug meta and bundle ids are set, attaches debug information`() { + val options = SentryOptions().apply { bundleIds.addAll(listOf("id1", "id2")) } + val debugMeta = DebugMeta.buildDebugMeta(DebugMeta(), options) + + assertNotNull(debugMeta) { + assertNotNull(it.images) { images -> + assertEquals("id1", images[0].debugId) + assertEquals("jvm", images[0].type) + assertEquals("id2", images[1].debugId) + assertEquals("jvm", images[1].type) + } + } + } + + @Test + fun `when event has debug meta as well as images and bundle ids are set, attaches debug information`() { + val options = SentryOptions().apply { bundleIds.addAll(listOf("id1", "id2")) } + val debugMeta = DebugMeta.buildDebugMeta(DebugMeta().also { it.images = listOf() }, options) + + assertNotNull(debugMeta) { + assertNotNull(it.images) { images -> + assertEquals("id1", images[0].debugId) + assertEquals("jvm", images[0].type) + assertEquals("id2", images[1].debugId) + assertEquals("jvm", images[1].type) + } + } + } }