diff --git a/CHANGELOG.md b/CHANGELOG.md index edb3fd6034..c79b99f723 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- Continuous Profiling - Add delayed stop ([#4293](https://github.com/getsentry/sentry-java/pull/4293)) - Continuous Profiling - Out of Experimental ([#4310](https://github.com/getsentry/sentry-java/pull/4310)) ## 8.6.0 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 0c66e1adfd..94be555104 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 @@ -11,6 +11,7 @@ import io.sentry.ILogger; import io.sentry.IScopes; import io.sentry.ISentryExecutorService; +import io.sentry.ISentryLifecycleToken; import io.sentry.NoOpScopes; import io.sentry.PerformanceCollectionData; import io.sentry.ProfileChunk; @@ -24,6 +25,7 @@ import io.sentry.android.core.internal.util.SentryFrameMetricsCollector; import io.sentry.protocol.SentryId; import io.sentry.transport.RateLimiter; +import io.sentry.util.AutoClosableReentrantLock; import io.sentry.util.SentryRandom; import java.util.ArrayList; import java.util.List; @@ -57,10 +59,14 @@ public class AndroidContinuousProfiler private @NotNull SentryId chunkId = SentryId.EMPTY_ID; private final @NotNull AtomicBoolean isClosed = new AtomicBoolean(false); private @NotNull SentryDate startProfileChunkTimestamp = new SentryNanotimeDate(); - private boolean shouldSample = true; + private volatile boolean shouldSample = true; + private boolean shouldStop = false; private boolean isSampled = false; private int rootSpanCounter = 0; + private final AutoClosableReentrantLock lock = new AutoClosableReentrantLock(); + private final AutoClosableReentrantLock payloadLock = new AutoClosableReentrantLock(); + public AndroidContinuousProfiler( final @NotNull BuildInfoProvider buildInfoProvider, final @NotNull SentryFrameMetricsCollector frameMetricsCollector, @@ -106,42 +112,46 @@ private void init() { } @Override - public synchronized void startProfiler( + public void startProfiler( final @NotNull ProfileLifecycle profileLifecycle, final @NotNull TracesSampler tracesSampler) { - if (shouldSample) { - isSampled = tracesSampler.sampleSessionProfile(SentryRandom.current().nextDouble()); - shouldSample = false; - } - if (!isSampled) { - logger.log(SentryLevel.DEBUG, "Profiler was not started due to sampling decision."); - return; - } - switch (profileLifecycle) { - case TRACE: - // rootSpanCounter should never be negative, unless the user changed profile lifecycle while - // the profiler is running or close() is called. This is just a safety check. - if (rootSpanCounter < 0) { - rootSpanCounter = 0; - } - rootSpanCounter++; - break; - case MANUAL: - // We check if the profiler is already running and log a message only in manual mode, since - // in trace mode we can have multiple concurrent traces - if (isRunning()) { - logger.log(SentryLevel.DEBUG, "Profiler is already running."); - return; - } - break; - } - if (!isRunning()) { - logger.log(SentryLevel.DEBUG, "Started Profiler."); - start(); + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + if (shouldSample) { + isSampled = tracesSampler.sampleSessionProfile(SentryRandom.current().nextDouble()); + shouldSample = false; + } + if (!isSampled) { + logger.log(SentryLevel.DEBUG, "Profiler was not started due to sampling decision."); + return; + } + switch (profileLifecycle) { + case TRACE: + // rootSpanCounter should never be negative, unless the user changed profile lifecycle + // while + // the profiler is running or close() is called. This is just a safety check. + if (rootSpanCounter < 0) { + rootSpanCounter = 0; + } + rootSpanCounter++; + break; + case MANUAL: + // We check if the profiler is already running and log a message only in manual mode, + // since + // in trace mode we can have multiple concurrent traces + if (isRunning()) { + logger.log(SentryLevel.DEBUG, "Profiler is already running."); + return; + } + break; + } + if (!isRunning()) { + logger.log(SentryLevel.DEBUG, "Started Profiler."); + start(); + } } } - private synchronized void start() { + private void start() { if ((scopes == null || scopes == NoOpScopes.getInstance()) && Sentry.getCurrentScopes() != NoOpScopes.getInstance()) { this.scopes = Sentry.getCurrentScopes(); @@ -213,103 +223,112 @@ private synchronized void start() { SentryLevel.ERROR, "Failed to schedule profiling chunk finish. Did you call Sentry.close()?", e); + shouldStop = true; } } @Override - public synchronized void stopProfiler(final @NotNull ProfileLifecycle profileLifecycle) { - switch (profileLifecycle) { - case TRACE: - rootSpanCounter--; - // If there are active spans, and profile lifecycle is trace, we don't stop the profiler - if (rootSpanCounter > 0) { - return; - } - // rootSpanCounter should never be negative, unless the user changed profile lifecycle while - // the profiler is running or close() is called. This is just a safety check. - if (rootSpanCounter < 0) { - rootSpanCounter = 0; - } - stop(false); - break; - case MANUAL: - stop(false); - break; + public void stopProfiler(final @NotNull ProfileLifecycle profileLifecycle) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + switch (profileLifecycle) { + case TRACE: + rootSpanCounter--; + // If there are active spans, and profile lifecycle is trace, we don't stop the profiler + if (rootSpanCounter > 0) { + return; + } + // rootSpanCounter should never be negative, unless the user changed profile lifecycle + // while the profiler is running or close() is called. This is just a safety check. + if (rootSpanCounter < 0) { + rootSpanCounter = 0; + } + shouldStop = true; + break; + case MANUAL: + shouldStop = true; + break; + } } } - private synchronized void stop(final boolean restartProfiler) { - if (stopFuture != null) { - stopFuture.cancel(true); - } - // check if profiler was created and it's running - if (profiler == null || !isRunning) { - // When the profiler is stopped due to an error (e.g. offline or rate limited), reset the ids - profilerId = SentryId.EMPTY_ID; - chunkId = SentryId.EMPTY_ID; - return; - } - - // onTransactionStart() is only available since Lollipop_MR1 - // and SystemClock.elapsedRealtimeNanos() since Jelly Bean - if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) { - return; - } + private void stop(final boolean restartProfiler) { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + if (stopFuture != null) { + stopFuture.cancel(true); + } + // check if profiler was created and it's running + if (profiler == null || !isRunning) { + // When the profiler is stopped due to an error (e.g. offline or rate limited), reset the + // ids + profilerId = SentryId.EMPTY_ID; + chunkId = SentryId.EMPTY_ID; + return; + } - List performanceCollectionData = null; - if (performanceCollector != null) { - performanceCollectionData = performanceCollector.stop(chunkId.toString()); - } + // onTransactionStart() is only available since Lollipop_MR1 + // and SystemClock.elapsedRealtimeNanos() since Jelly Bean + if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) { + return; + } - final AndroidProfiler.ProfileEndData endData = - profiler.endAndCollect(false, performanceCollectionData); + List performanceCollectionData = null; + if (performanceCollector != null) { + performanceCollectionData = performanceCollector.stop(chunkId.toString()); + } - // check if profiler end successfully - if (endData == null) { - logger.log( - SentryLevel.ERROR, - "An error occurred while collecting a profile chunk, and it won't be sent."); - } else { - // The scopes can be null if the profiler is started before the SDK is initialized (app start - // profiling), meaning there's no scopes to send the chunks. In that case, we store the data - // in a list and send it when the next chunk is finished. - synchronized (payloadBuilders) { - payloadBuilders.add( - new ProfileChunk.Builder( - profilerId, - chunkId, - endData.measurementsMap, - endData.traceFile, - startProfileChunkTimestamp)); + final AndroidProfiler.ProfileEndData endData = + profiler.endAndCollect(false, performanceCollectionData); + + // check if profiler end successfully + if (endData == null) { + logger.log( + SentryLevel.ERROR, + "An error occurred while collecting a profile chunk, and it won't be sent."); + } else { + // The scopes can be null if the profiler is started before the SDK is initialized (app + // start profiling), meaning there's no scopes to send the chunks. In that case, we store + // the data in a list and send it when the next chunk is finished. + try (final @NotNull ISentryLifecycleToken ignored2 = payloadLock.acquire()) { + payloadBuilders.add( + new ProfileChunk.Builder( + profilerId, + chunkId, + endData.measurementsMap, + endData.traceFile, + startProfileChunkTimestamp)); + } } - } - isRunning = false; - // A chunk is finished. Next chunk will have a different id. - chunkId = SentryId.EMPTY_ID; + isRunning = false; + // A chunk is finished. Next chunk will have a different id. + chunkId = SentryId.EMPTY_ID; - if (scopes != null) { - sendChunks(scopes, scopes.getOptions()); - } + if (scopes != null) { + sendChunks(scopes, scopes.getOptions()); + } - if (restartProfiler) { - logger.log(SentryLevel.DEBUG, "Profile chunk finished. Starting a new one."); - start(); - } else { - // When the profiler is stopped manually, we have to reset its id - profilerId = SentryId.EMPTY_ID; - logger.log(SentryLevel.DEBUG, "Profile chunk finished."); + if (restartProfiler && !shouldStop) { + logger.log(SentryLevel.DEBUG, "Profile chunk finished. Starting a new one."); + start(); + } else { + // When the profiler is stopped manually, we have to reset its id + profilerId = SentryId.EMPTY_ID; + logger.log(SentryLevel.DEBUG, "Profile chunk finished."); + } } } - public synchronized void reevaluateSampling() { + public void reevaluateSampling() { shouldSample = true; } - public synchronized void close() { - rootSpanCounter = 0; - stop(false); - isClosed.set(true); + public void close() { + try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { + rootSpanCounter = 0; + shouldStop = true; + stop(false); + isClosed.set(true); + } } @Override @@ -328,7 +347,7 @@ private void sendChunks(final @NotNull IScopes scopes, final @NotNull SentryOpti return; } final ArrayList payloads = new ArrayList<>(payloadBuilders.size()); - synchronized (payloadBuilders) { + try (final @NotNull ISentryLifecycleToken ignored = payloadLock.acquire()) { for (ProfileChunk.Builder builder : payloadBuilders) { payloads.add(builder.build(options)); } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidContinuousProfilerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidContinuousProfilerTest.kt index daf7c84d15..4e1b45ebb0 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidContinuousProfilerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidContinuousProfilerTest.kt @@ -10,7 +10,6 @@ import io.sentry.DataCategory import io.sentry.IConnectionStatusProvider import io.sentry.ILogger import io.sentry.IScopes -import io.sentry.ISentryExecutorService import io.sentry.MemoryCollectionData import io.sentry.PerformanceCollectionData import io.sentry.ProfileLifecycle @@ -39,7 +38,6 @@ import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import java.io.File -import java.util.concurrent.Callable import java.util.concurrent.Future import kotlin.test.AfterTest import kotlin.test.BeforeTest @@ -61,6 +59,7 @@ class AndroidContinuousProfilerTest { val buildInfo = mock { whenever(it.sdkInfoVersion).thenReturn(Build.VERSION_CODES.LOLLIPOP_MR1) } + val executor = DeferredExecutorService() val mockedSentry = mockStatic(Sentry::class.java) val mockLogger = mock() val mockTracesSampler = mock() @@ -84,6 +83,7 @@ class AndroidContinuousProfilerTest { } fun getSut(buildInfoProvider: BuildInfoProvider = buildInfo, optionConfig: ((options: SentryAndroidOptions) -> Unit) = {}): AndroidContinuousProfiler { + options.executorService = executor optionConfig(options) whenever(scopes.options).thenReturn(options) transaction1 = SentryTracer(TransactionContext("", ""), scopes) @@ -152,6 +152,20 @@ class AndroidContinuousProfilerTest { profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) assertTrue(profiler.isRunning) profiler.stopProfiler(ProfileLifecycle.MANUAL) + fixture.executor.runAll() + assertFalse(profiler.isRunning) + } + + @Test + fun `stopProfiler stops the profiler after chunk is finished`() { + val profiler = fixture.getSut() + profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) + assertTrue(profiler.isRunning) + // We are scheduling the profiler to stop at the end of the chunk, so it should still be running + profiler.stopProfiler(ProfileLifecycle.MANUAL) + assertTrue(profiler.isRunning) + // We run the executor service to trigger the chunk finish, and the profiler shouldn't restart + fixture.executor.runAll() assertFalse(profiler.isRunning) } @@ -183,11 +197,13 @@ class AndroidContinuousProfilerTest { // rootSpanCounter is decremented when the profiler stops in trace mode, and keeps running until rootSpanCounter is 0 profiler.stopProfiler(ProfileLifecycle.TRACE) + fixture.executor.runAll() assertEquals(1, profiler.rootSpanCounter) assertTrue(profiler.isRunning) // only when rootSpanCounter is 0 the profiler stops profiler.stopProfiler(ProfileLifecycle.TRACE) + fixture.executor.runAll() assertEquals(0, profiler.rootSpanCounter) assertFalse(profiler.isRunning) } @@ -316,19 +332,6 @@ class AndroidContinuousProfilerTest { assertFalse(profiler.isRunning) } - @Test - fun `profiler never use background threads`() { - val mockExecutorService: ISentryExecutorService = mock() - val profiler = fixture.getSut { - it.executorService = mockExecutorService - } - whenever(mockExecutorService.submit(any>())).thenReturn(mock()) - profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) - verify(mockExecutorService, never()).submit(any()) - profiler.stopProfiler(ProfileLifecycle.MANUAL) - verify(mockExecutorService, never()).submit(any>()) - } - @Test fun `profiler does not throw if traces cannot be written to disk`() { val profiler = fixture.getSut { @@ -336,6 +339,7 @@ class AndroidContinuousProfilerTest { } profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) profiler.stopProfiler(ProfileLifecycle.MANUAL) + fixture.executor.runAll() // We assert that no trace files are written assertTrue( File(fixture.options.profilingTracesDirPath!!) @@ -363,6 +367,7 @@ class AndroidContinuousProfilerTest { profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) verify(performanceCollector, never()).stop(any()) profiler.stopProfiler(ProfileLifecycle.MANUAL) + fixture.executor.runAll() verify(performanceCollector).stop(any()) } @@ -374,6 +379,7 @@ class AndroidContinuousProfilerTest { profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) verify(fixture.frameMetricsCollector, never()).stopCollection(frameMetricsCollectorId) profiler.stopProfiler(ProfileLifecycle.MANUAL) + fixture.executor.runAll() verify(fixture.frameMetricsCollector).stopCollection(frameMetricsCollectorId) } @@ -393,46 +399,39 @@ class AndroidContinuousProfilerTest { val stopFuture = profiler.stopFuture assertNotNull(stopFuture) - assertTrue(stopFuture.isCancelled) + assertTrue(stopFuture.isCancelled || stopFuture.isDone) } @Test fun `profiler stops and restart for each chunk`() { - val executorService = DeferredExecutorService() - val profiler = fixture.getSut { - it.executorService = executorService - } + val profiler = fixture.getSut() profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) assertTrue(profiler.isRunning) - executorService.runAll() + fixture.executor.runAll() verify(fixture.mockLogger).log(eq(SentryLevel.DEBUG), eq("Profile chunk finished. Starting a new one.")) assertTrue(profiler.isRunning) - executorService.runAll() + fixture.executor.runAll() verify(fixture.mockLogger, times(2)).log(eq(SentryLevel.DEBUG), eq("Profile chunk finished. Starting a new one.")) assertTrue(profiler.isRunning) } @Test fun `profiler sends chunk on each restart`() { - val executorService = DeferredExecutorService() - val profiler = fixture.getSut { - it.executorService = executorService - } + val profiler = fixture.getSut() profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) assertTrue(profiler.isRunning) // We run the executor service to trigger the profiler restart (chunk finish) - executorService.runAll() + fixture.executor.runAll() verify(fixture.scopes, never()).captureProfileChunk(any()) // Now the executor is used to send the chunk - executorService.runAll() + fixture.executor.runAll() verify(fixture.scopes).captureProfileChunk(any()) } @Test fun `profiler sends chunk with measurements`() { - val executorService = DeferredExecutorService() val performanceCollector = mock() val collectionData = PerformanceCollectionData() @@ -441,13 +440,13 @@ class AndroidContinuousProfilerTest { whenever(performanceCollector.stop(any())).thenReturn(listOf(collectionData)) fixture.options.compositePerformanceCollector = performanceCollector - val profiler = fixture.getSut { - it.executorService = executorService - } + val profiler = fixture.getSut() profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) profiler.stopProfiler(ProfileLifecycle.MANUAL) - // We run the executor service to send the profile chunk - executorService.runAll() + // We run the executor service to stop the profiler + fixture.executor.runAll() + // Then we run it again to send the profile chunk + fixture.executor.runAll() verify(fixture.scopes).captureProfileChunk( check { assertContains(it.measurements, ProfileMeasurement.ID_CPU_USAGE) @@ -459,28 +458,21 @@ class AndroidContinuousProfilerTest { @Test fun `profiler sends another chunk on stop`() { - val executorService = DeferredExecutorService() - val profiler = fixture.getSut { - it.executorService = executorService - } + val profiler = fixture.getSut() profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) assertTrue(profiler.isRunning) // We run the executor service to trigger the profiler restart (chunk finish) - executorService.runAll() + fixture.executor.runAll() verify(fixture.scopes, never()).captureProfileChunk(any()) - // We stop the profiler, which should send an additional chunk profiler.stopProfiler(ProfileLifecycle.MANUAL) - // Now the executor is used to send the chunk - executorService.runAll() - verify(fixture.scopes, times(2)).captureProfileChunk(any()) + // We stop the profiler, which should send a chunk + fixture.executor.runAll() + verify(fixture.scopes).captureProfileChunk(any()) } @Test fun `profiler does not send chunks after close`() { - val executorService = DeferredExecutorService() - val profiler = fixture.getSut { - it.executorService = executorService - } + val profiler = fixture.getSut() profiler.startProfiler(ProfileLifecycle.MANUAL, fixture.mockTracesSampler) assertTrue(profiler.isRunning) @@ -488,16 +480,13 @@ class AndroidContinuousProfilerTest { profiler.close() // The executor used to send the chunk doesn't do anything - executorService.runAll() + fixture.executor.runAll() verify(fixture.scopes, never()).captureProfileChunk(any()) } @Test fun `profiler stops when rate limited`() { - val executorService = DeferredExecutorService() - val profiler = fixture.getSut { - it.executorService = executorService - } + val profiler = fixture.getSut() val rateLimiter = mock() whenever(rateLimiter.isActiveForCategory(DataCategory.ProfileChunk)).thenReturn(true) @@ -513,10 +502,7 @@ class AndroidContinuousProfilerTest { @Test fun `profiler does not start when rate limited`() { - val executorService = DeferredExecutorService() - val profiler = fixture.getSut { - it.executorService = executorService - } + val profiler = fixture.getSut() val rateLimiter = mock() whenever(rateLimiter.isActiveForCategory(DataCategory.ProfileChunk)).thenReturn(true) whenever(fixture.scopes.rateLimiter).thenReturn(rateLimiter) @@ -530,9 +516,7 @@ class AndroidContinuousProfilerTest { @Test fun `profiler does not start when offline`() { - val executorService = DeferredExecutorService() val profiler = fixture.getSut { - it.executorService = executorService it.connectionStatusProvider = mock { provider -> whenever(provider.connectionStatus).thenReturn(IConnectionStatusProvider.ConnectionStatus.DISCONNECTED) } diff --git a/sentry-test-support/src/main/kotlin/io/sentry/test/Mocks.kt b/sentry-test-support/src/main/kotlin/io/sentry/test/Mocks.kt index b30fe3464f..d048b42d42 100644 --- a/sentry-test-support/src/main/kotlin/io/sentry/test/Mocks.kt +++ b/sentry-test-support/src/main/kotlin/io/sentry/test/Mocks.kt @@ -13,6 +13,7 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import java.util.concurrent.Callable import java.util.concurrent.Future +import java.util.concurrent.FutureTask import java.util.concurrent.atomic.AtomicBoolean class ImmediateExecutorService : ISentryExecutorService { @@ -58,7 +59,7 @@ class DeferredExecutorService : ISentryExecutorService { synchronized(this) { runnables.add(runnable) } - return mock() + return FutureTask {} } override fun submit(callable: Callable): Future = mock() @@ -66,7 +67,7 @@ class DeferredExecutorService : ISentryExecutorService { synchronized(this) { scheduledRunnables.add(runnable) } - return mock() + return FutureTask {} } override fun close(timeoutMillis: Long) {} override fun isClosed(): Boolean = false diff --git a/sentry/src/main/java/io/sentry/SentryTracer.java b/sentry/src/main/java/io/sentry/SentryTracer.java index 8da554cd3e..0496f40721 100644 --- a/sentry/src/main/java/io/sentry/SentryTracer.java +++ b/sentry/src/main/java/io/sentry/SentryTracer.java @@ -81,6 +81,8 @@ public SentryTracer( this.transactionNameSource = context.getTransactionNameSource(); this.transactionOptions = transactionOptions; + setDefaultSpanData(root); + final @NotNull SentryId continuousProfilerId = scopes.getOptions().getContinuousProfiler().getProfilerId(); if (!continuousProfilerId.equals(SentryId.EMPTY_ID) && Boolean.TRUE.equals(isSampled())) { @@ -519,14 +521,7 @@ private ISpan createChild( // } // }); // span.setDescription(description); - final @NotNull IThreadChecker threadChecker = scopes.getOptions().getThreadChecker(); - final SentryId profilerId = scopes.getOptions().getContinuousProfiler().getProfilerId(); - if (!profilerId.equals(SentryId.EMPTY_ID) && Boolean.TRUE.equals(span.isSampled())) { - span.setData(SpanDataConvention.PROFILER_ID, profilerId.toString()); - } - span.setData( - SpanDataConvention.THREAD_ID, String.valueOf(threadChecker.currentThreadSystemId())); - span.setData(SpanDataConvention.THREAD_NAME, threadChecker.getCurrentThreadName()); + setDefaultSpanData(span); this.children.add(span); if (compositePerformanceCollector != null) { compositePerformanceCollector.onSpanStarted(span); @@ -545,6 +540,19 @@ private ISpan createChild( } } + /** Sets the default data in the span, including profiler _id, thread id and thread name */ + private void setDefaultSpanData(final @NotNull ISpan span) { + final @NotNull IThreadChecker threadChecker = scopes.getOptions().getThreadChecker(); + final @NotNull SentryId profilerId = + scopes.getOptions().getContinuousProfiler().getProfilerId(); + if (!profilerId.equals(SentryId.EMPTY_ID) && Boolean.TRUE.equals(span.isSampled())) { + span.setData(SpanDataConvention.PROFILER_ID, profilerId.toString()); + } + span.setData( + SpanDataConvention.THREAD_ID, String.valueOf(threadChecker.currentThreadSystemId())); + span.setData(SpanDataConvention.THREAD_NAME, threadChecker.getCurrentThreadName()); + } + @Override public @NotNull ISpan startChild(final @NotNull String operation) { return this.startChild(operation, (String) null); diff --git a/sentry/src/test/java/io/sentry/SentryTracerTest.kt b/sentry/src/test/java/io/sentry/SentryTracerTest.kt index 4af0b033e7..85bbf0fd90 100644 --- a/sentry/src/test/java/io/sentry/SentryTracerTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTracerTest.kt @@ -86,6 +86,15 @@ class SentryTracerTest { assertEquals("new-origin", transaction.spanContext.origin) } + @Test + fun `root span has thread name and thread id in the data`() { + val tracer = fixture.getSut() + assertTrue(tracer.root.data.containsKey(SpanDataConvention.THREAD_NAME)) + assertTrue(tracer.root.data.containsKey(SpanDataConvention.THREAD_ID)) + assertTrue(tracer.data!!.containsKey(SpanDataConvention.THREAD_NAME)) + assertTrue(tracer.data!!.containsKey(SpanDataConvention.THREAD_ID)) + } + @Test fun `does not create child span if origin is ignored`() { val tracer = fixture.getSut({