diff --git a/sentry-async-profiler/api/sentry-async-profiler.api b/sentry-async-profiler/api/sentry-async-profiler.api index b8d0a06f0c3..af2962cbd61 100644 --- a/sentry-async-profiler/api/sentry-async-profiler.api +++ b/sentry-async-profiler/api/sentry-async-profiler.api @@ -4,7 +4,7 @@ public final class io/sentry/asyncprofiler/BuildConfig { } public final class io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverter : io/sentry/asyncprofiler/vendor/asyncprofiler/convert/JfrConverter { - public fun (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrReader;Lio/sentry/asyncprofiler/vendor/asyncprofiler/convert/Arguments;Lio/sentry/SentryStackTraceFactory;)V + public fun (Lio/sentry/asyncprofiler/vendor/asyncprofiler/jfr/JfrReader;Lio/sentry/asyncprofiler/vendor/asyncprofiler/convert/Arguments;Lio/sentry/SentryStackTraceFactory;Lio/sentry/ILogger;)V public static fun convertFromFileStatic (Ljava/nio/file/Path;)Lio/sentry/protocol/profiling/SentryProfile; } diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverter.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverter.java index d90370c2e88..4489497e815 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverter.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverter.java @@ -1,7 +1,9 @@ package io.sentry.asyncprofiler.convert; import io.sentry.DateUtils; +import io.sentry.ILogger; import io.sentry.Sentry; +import io.sentry.SentryLevel; import io.sentry.SentryStackTraceFactory; import io.sentry.asyncprofiler.vendor.asyncprofiler.convert.Arguments; import io.sentry.asyncprofiler.vendor.asyncprofiler.convert.JfrConverter; @@ -24,16 +26,22 @@ public final class JfrAsyncProfilerToSentryProfileConverter extends JfrConverter { private static final double NANOS_PER_SECOND = 1_000_000_000.0; + private static final long UNKNOWN_THREAD_ID = -1; private final @NotNull SentryProfile sentryProfile = new SentryProfile(); private final @NotNull SentryStackTraceFactory stackTraceFactory; + private final @NotNull ILogger logger; private final @NotNull Map frameDeduplicationMap = new HashMap<>(); private final @NotNull Map, Integer> stackDeduplicationMap = new HashMap<>(); public JfrAsyncProfilerToSentryProfileConverter( - JfrReader jfr, Arguments args, @NotNull SentryStackTraceFactory stackTraceFactory) { + JfrReader jfr, + Arguments args, + @NotNull SentryStackTraceFactory stackTraceFactory, + @NotNull ILogger logger) { super(jfr, args); this.stackTraceFactory = stackTraceFactory; + this.logger = logger; } @Override @@ -60,7 +68,9 @@ protected EventCollector createCollector(Arguments args) { SentryStackTraceFactory stackTraceFactory = new SentryStackTraceFactory(Sentry.getGlobalScope().getOptions()); - converter = new JfrAsyncProfilerToSentryProfileConverter(jfrReader, args, stackTraceFactory); + ILogger logger = Sentry.getGlobalScope().getOptions().getLogger(); + converter = + new JfrAsyncProfilerToSentryProfileConverter(jfrReader, args, stackTraceFactory, logger); converter.convert(); } @@ -88,25 +98,32 @@ public ProfileEventVisitor( @Override public void visit(Event event, long samples, long value) { - StackTrace stackTrace = jfr.stackTraces.get(event.stackTraceId); - long threadId = resolveThreadId(event.tid); + try { + StackTrace stackTrace = jfr.stackTraces.get(event.stackTraceId); + long threadId = resolveThreadId(event.tid); - if (stackTrace != null) { - if (args.threads) { - processThreadMetadata(event, threadId); - } + if (stackTrace != null) { + if (args.threads) { + processThreadMetadata(event, threadId); + } - processSampleWithStack(event, threadId, stackTrace); + processSampleWithStack(event, threadId, stackTrace); + } + } catch (Exception e) { + logger.log(SentryLevel.WARNING, "Failed to process JFR event " + event, e); } } - private long resolveThreadId(int eventThreadId) { - return jfr.threads.get(eventThreadId) != null - ? jfr.javaThreads.get(eventThreadId) - : eventThreadId; + private long resolveThreadId(int eventId) { + Long javaThreadId = jfr.javaThreads.get(eventId); + return javaThreadId != null ? javaThreadId : UNKNOWN_THREAD_ID; } private void processThreadMetadata(Event event, long threadId) { + if (threadId == UNKNOWN_THREAD_ID) { + return; + } + final String threadName = getPlainThreadName(event.tid); sentryProfile .getThreadMetadata() @@ -167,7 +184,6 @@ private List createFramesAndCallStack(StackTrace stackTrace) { } SentryStackFrame frame = createStackFrame(element); - frame.setNative(isNativeFrame(types[i])); int frameIndex = getOrAddFrame(frame); callStack.add(frameIndex); } diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfiler.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfiler.java index 896fab6c3e3..6b45d568394 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfiler.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfiler.java @@ -56,9 +56,8 @@ public final class JavaContinuousProfiler private @NotNull String filename = ""; - private @Nullable AsyncProfiler profiler; + private @NotNull AsyncProfiler profiler; private volatile boolean shouldSample = true; - private boolean shouldStop = false; private boolean isSampled = false; private int rootSpanCounter = 0; @@ -69,7 +68,8 @@ public JavaContinuousProfiler( final @NotNull ILogger logger, final @Nullable String profilingTracesDirPath, final int profilingTracesHz, - final @NotNull ISentryExecutorService executorService) { + final @NotNull ISentryExecutorService executorService) + throws Exception { this.logger = logger; this.profilingTracesDirPath = profilingTracesDirPath; this.profilingTracesHz = profilingTracesHz; @@ -77,19 +77,11 @@ public JavaContinuousProfiler( initializeProfiler(); } - private void initializeProfiler() { - try { - this.profiler = AsyncProfiler.getInstance(); - // Check version to verify profiler is working - String version = profiler.execute("version"); - logger.log(SentryLevel.DEBUG, "AsyncProfiler initialized successfully. Version: " + version); - } catch (Exception e) { - logger.log( - SentryLevel.WARNING, - "Failed to initialize AsyncProfiler. Profiling will be disabled.", - e); - this.profiler = null; - } + private void initializeProfiler() throws Exception { + this.profiler = AsyncProfiler.getInstance(); + // Check version to verify profiler is working + String version = profiler.execute("version"); + logger.log(SentryLevel.DEBUG, "AsyncProfiler initialized successfully. Version: " + version); } private boolean init() { @@ -98,11 +90,6 @@ private boolean init() { } isInitialized = true; - if (profiler == null) { - logger.log(SentryLevel.ERROR, "Disabling profiling because AsyncProfiler is not available."); - return false; - } - if (profilingTracesDirPath == null) { logger.log( SentryLevel.WARNING, @@ -115,8 +102,10 @@ private boolean init() { if (!profileDir.canWrite() || !profileDir.exists()) { logger.log( SentryLevel.WARNING, - "Disabling profiling because traces directory is not writable or does not exist: %s", - profilingTracesDirPath); + "Disabling profiling because traces directory is not writable or does not exist: %s (writable=%b, exists=%b)", + profilingTracesDirPath, + profileDir.canWrite(), + profileDir.exists()); return false; } @@ -166,10 +155,11 @@ public void startProfiler( } if (!isRunning()) { - shouldStop = false; logger.log(SentryLevel.DEBUG, "Started Profiler."); start(); } + } catch (Exception e) { + logger.log(SentryLevel.ERROR, "Error starting profiler: ", e); } } @@ -208,11 +198,6 @@ private void start() { startProfileChunkTimestamp = new SentryNanotimeDate(); } - if (profiler == null) { - logger.log(SentryLevel.ERROR, "Cannot start profiling: AsyncProfiler is not available"); - return; - } - filename = profilingTracesDirPath + File.separator + SentryUUID.generateSentryId() + ".jfr"; File jfrFile = new File(filename); @@ -231,9 +216,7 @@ private void start() { logger.log(SentryLevel.ERROR, "Failed to start profiling: ", e); filename = ""; // Try to clean up the file if it was created - if (jfrFile.exists()) { - jfrFile.delete(); - } + safelyRemoveFile(jfrFile); return; } @@ -250,7 +233,8 @@ private void start() { SentryLevel.ERROR, "Failed to schedule profiling chunk finish. Did you call Sentry.close()?", e); - shouldStop = true; + // If we can't schedule the auto-stop, stop immediately without restart + stop(false); } } @@ -269,10 +253,12 @@ public void stopProfiler(final @NotNull ProfileLifecycle profileLifecycle) { if (rootSpanCounter < 0) { rootSpanCounter = 0; } - shouldStop = true; + // Stop immediately without restart + stop(false); break; case MANUAL: - shouldStop = true; + // Stop immediately without restart + stop(false); break; } } @@ -293,19 +279,12 @@ private void stop(final boolean restartProfiler) { File jfrFile = new File(filename); - if (profiler == null) { - logger.log(SentryLevel.WARNING, "Profiler is null when trying to stop"); - return; - } - try { profiler.execute("stop,jfr"); } catch (Exception e) { logger.log(SentryLevel.ERROR, "Error stopping profiler, attempting cleanup: ", e); // Clean up file if it exists - if (jfrFile.exists()) { - jfrFile.delete(); - } + safelyRemoveFile(jfrFile); } // The scopes can be null if the profiler is started before the SDK is initialized (app @@ -330,9 +309,7 @@ private void stop(final boolean restartProfiler) { jfrFile.exists(), jfrFile.canRead(), jfrFile.length()); - if (jfrFile.exists()) { - jfrFile.delete(); - } + safelyRemoveFile(jfrFile); } // Always clean up state, even if stop failed @@ -343,7 +320,7 @@ private void stop(final boolean restartProfiler) { sendChunks(scopes, scopes.getOptions()); } - if (restartProfiler && !shouldStop) { + if (restartProfiler) { logger.log(SentryLevel.DEBUG, "Profile chunk finished. Starting a new one."); start(); } else { @@ -351,6 +328,8 @@ private void stop(final boolean restartProfiler) { profilerId = SentryId.EMPTY_ID; logger.log(SentryLevel.DEBUG, "Profile chunk finished."); } + } catch (Exception e) { + logger.log(SentryLevel.ERROR, "Error stopping profiler: ", e); } } @@ -363,9 +342,8 @@ public void reevaluateSampling() { public void close(final boolean isTerminating) { try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { rootSpanCounter = 0; - shouldStop = true; + stop(false); if (isTerminating) { - stop(false); isClosed.set(true); } } @@ -403,10 +381,20 @@ private void sendChunks(final @NotNull IScopes scopes, final @NotNull SentryOpti } } + private void safelyRemoveFile(File file) { + try { + if (file.exists()) { + file.delete(); + } + } catch (Exception e) { + logger.log(SentryLevel.INFO, "Failed to remove jfr file %s.", file.getAbsolutePath(), e); + } + } + @Override public boolean isRunning() { try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) { - return isRunning && profiler != null && !filename.isEmpty(); + return isRunning && !filename.isEmpty(); } } diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerContinuousProfilerProvider.java b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerContinuousProfilerProvider.java index 49d83cffb3d..226cfc09084 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerContinuousProfilerProvider.java +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerContinuousProfilerProvider.java @@ -3,6 +3,8 @@ import io.sentry.IContinuousProfiler; import io.sentry.ILogger; import io.sentry.ISentryExecutorService; +import io.sentry.NoOpContinuousProfiler; +import io.sentry.SentryLevel; import io.sentry.asyncprofiler.profiling.JavaContinuousProfiler; import io.sentry.profiling.JavaContinuousProfilerProvider; import io.sentry.profiling.JavaProfileConverterProvider; @@ -22,7 +24,15 @@ public final class AsyncProfilerContinuousProfilerProvider String profilingTracesDirPath, int profilingTracesHz, ISentryExecutorService executorService) { - return new JavaContinuousProfiler( - logger, profilingTracesDirPath, profilingTracesHz, executorService); + try { + return new JavaContinuousProfiler( + logger, profilingTracesDirPath, profilingTracesHz, executorService); + } catch (Exception e) { + logger.log( + SentryLevel.WARNING, + "Failed to initialize AsyncProfiler. Profiling will be disabled.", + e); + return NoOpContinuousProfiler.getInstance(); + } } } diff --git a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/README.md b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/README.md index 85eff757853..733a69f1c3b 100644 --- a/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/README.md +++ b/sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/vendor/asyncprofiler/README.md @@ -1,4 +1,4 @@ # Vendored AsyncProfiler code for converting JFR Files -- Vendored-in from commit fe1bc66d4b6181413847f6bbe5c0db805f3e9194 of repository: git@github.com:async-profiler/async-profiler.git +- Vendored-in from commit https://github.com/async-profiler/async-profiler/tree/fe1bc66d4b6181413847f6bbe5c0db805f3e9194 - Only the code related to JFR conversion is included. - The `AsyncProfiler` itself is included as a dependency in the Maven project. diff --git a/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverterTest.kt b/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverterTest.kt index 6e11ad3cb5b..2b9c8ae1104 100644 --- a/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverterTest.kt +++ b/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverterTest.kt @@ -1,5 +1,6 @@ package io.sentry.asyncprofiler.convert +import io.sentry.DateUtils import io.sentry.ILogger import io.sentry.IProfileConverter import io.sentry.IScope @@ -12,7 +13,12 @@ import io.sentry.asyncprofiler.provider.AsyncProfilerProfileConverterProvider import io.sentry.protocol.profiling.SentryProfile import io.sentry.test.DeferredExecutorService import java.io.IOException +import java.time.LocalDateTime +import java.time.ZoneOffset +import java.time.temporal.ChronoUnit +import java.util.* import kotlin.io.path.Path +import kotlin.math.absoluteValue import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.assertEquals @@ -159,22 +165,21 @@ class JfrAsyncProfilerToSentryProfileConverterTest { val samples = sentryProfile.samples assertTrue(samples.isNotEmpty()) - // Verify timestamps are in seconds with proper decimal places - samples.forEach { sample -> - val timestamp = sample.timestamp - assertTrue(timestamp > 0, "Timestamp should be positive") - // No need to check exact decimal places as this depends on JFR precision - assertTrue( - timestamp < System.currentTimeMillis() / 1000.0 + 360, - "Timestamp should be recent", - ) - } + val minTimestamp = samples.minOf { it.timestamp } + val maxTimestamp = samples.maxOf { it.timestamp } + val sampleTimeStamp = + DateUtils.nanosToDate((maxTimestamp * 1000 * 1000 * 1000).toLong()).toInstant() - if (samples.isNotEmpty()) { - val minTimestamp = samples.minOf { it.timestamp } - val maxTimestamp = samples.maxOf { it.timestamp } - assertTrue(maxTimestamp >= minTimestamp, "Max timestamp should be >= min timestamp") - } + // The sample was recorded around "2025-09-05T08:14:50" in UTC timezone + val referenceTimestamp = LocalDateTime.parse("2025-09-05T08:14:50").toInstant(ZoneOffset.UTC) + val between = ChronoUnit.MILLIS.between(sampleTimeStamp, referenceTimestamp).absoluteValue + + assertTrue(between < 5000, "Sample timestamp should be within 5s of reference timestamp") + assertTrue(maxTimestamp >= minTimestamp, "Max timestamp should be >= min timestamp") + assertTrue( + maxTimestamp - minTimestamp <= 10, + "There should be a max difference of <10s between min and max timestamp", + ) } @Test diff --git a/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfilerTest.kt b/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfilerTest.kt index 5c7c58d3884..86f5d51fee2 100644 --- a/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfilerTest.kt +++ b/sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfilerTest.kt @@ -117,9 +117,6 @@ class JavaContinuousProfilerTest { 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) } @@ -279,8 +276,12 @@ class JavaContinuousProfilerTest { verify(fixture.mockLogger) .log( eq(SentryLevel.WARNING), - eq("Disabling profiling because traces directory is not writable or does not exist: %s"), + eq( + "Disabling profiling because traces directory is not writable or does not exist: %s (writable=%b, exists=%b)" + ), eq(expectedPath), + eq(false), + eq(true), ) } @@ -321,11 +322,12 @@ class JavaContinuousProfilerTest { assertTrue(profiler.isRunning) // We run the executor service to trigger the profiler restart (chunk finish) fixture.executor.runAll() + // At this point the chunk has been submitted to the executor, but yet to be sent verify(fixture.scopes, never()).captureProfileChunk(any()) profiler.stopProfiler(ProfileLifecycle.MANUAL) - // We stop the profiler, which should send a chunk + // We stop the profiler, which should send both the first and last chunk fixture.executor.runAll() - verify(fixture.scopes).captureProfileChunk(any()) + verify(fixture.scopes, times(2)).captureProfileChunk(any()) } @Test @@ -333,15 +335,11 @@ class JavaContinuousProfilerTest { val profiler = fixture.getSut() profiler.startProfiler(ProfileLifecycle.TRACE, fixture.mockTracesSampler) assertTrue(profiler.isRunning) - // We are scheduling the profiler to stop at the end of the chunk, so it should still be running + // We are closing the profiler, which should stop all profiles after the chunk is finished profiler.close(false) - assertTrue(profiler.isRunning) + assertFalse(profiler.isRunning) // However, close() already resets the rootSpanCounter assertEquals(0, profiler.rootSpanCounter) - - // We run the executor service to trigger the chunk finish, and the profiler shouldn't restart - fixture.executor.runAll() - assertFalse(profiler.isRunning) } @Test diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentrySpanProcessor.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentrySpanProcessor.java index 9b260cfc46c..bb374f4a517 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentrySpanProcessor.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/OtelSentrySpanProcessor.java @@ -117,6 +117,9 @@ public void onStart(final @NotNull Context parentContext, final @NotNull ReadWri sentryParentSpanId, baggage); sentrySpan.getSpanContext().setOrigin(SentrySpanExporter.TRACE_ORIGIN); + sentrySpan + .getSpanContext() + .setProfilerId(scopes.getOptions().getContinuousProfiler().getProfilerId()); spanStorage.storeSentrySpan(spanContext, sentrySpan); } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index fdf8d871ab3..6814fc1de76 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -3967,6 +3967,7 @@ public class io/sentry/SpanContext : io/sentry/JsonSerializable, io/sentry/JsonU public fun getOrigin ()Ljava/lang/String; public fun getParentSpanId ()Lio/sentry/SpanId; public fun getProfileSampled ()Ljava/lang/Boolean; + public fun getProfilerId ()Lio/sentry/protocol/SentryId; public fun getSampled ()Ljava/lang/Boolean; public fun getSamplingDecision ()Lio/sentry/TracesSamplingDecision; public fun getSpanId ()Lio/sentry/SpanId; @@ -3981,6 +3982,7 @@ public class io/sentry/SpanContext : io/sentry/JsonSerializable, io/sentry/JsonU public fun setInstrumenter (Lio/sentry/Instrumenter;)V public fun setOperation (Ljava/lang/String;)V public fun setOrigin (Ljava/lang/String;)V + public fun setProfilerId (Lio/sentry/protocol/SentryId;)V public fun setSampled (Ljava/lang/Boolean;)V public fun setSampled (Ljava/lang/Boolean;Ljava/lang/Boolean;)V public fun setSamplingDecision (Lio/sentry/TracesSamplingDecision;)V diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index 491da449ca1..1b44d3d80f2 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -938,17 +938,18 @@ public void flush(long timeoutMillis) { final @NotNull ISpanFactory spanFactory = maybeSpanFactory == null ? getOptions().getSpanFactory() : maybeSpanFactory; - // If continuous profiling is enabled in trace mode, let's start it. Profiler will sample on - // its own. + // If continuous profiling is enabled in trace mode, let's start it unless skipProfiling is + // true in TransactionOptions. + // Profiler will sample on its own. // Profiler is started before the transaction is created, so that the profiler id is available // when the transaction starts - if (samplingDecision.getSampled()) { - if (getOptions().isContinuousProfilingEnabled() - && getOptions().getProfileLifecycle() == ProfileLifecycle.TRACE) { - getOptions() - .getContinuousProfiler() - .startProfiler(ProfileLifecycle.TRACE, getOptions().getInternalTracesSampler()); - } + if (samplingDecision.getSampled() + && getOptions().isContinuousProfilingEnabled() + && getOptions().getProfileLifecycle() == ProfileLifecycle.TRACE + && transactionContext.getProfilerId().equals(SentryId.EMPTY_ID)) { + getOptions() + .getContinuousProfiler() + .startProfiler(ProfileLifecycle.TRACE, getOptions().getInternalTracesSampler()); } transaction = diff --git a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java index 3e34455fa07..0c9b07f7cd0 100644 --- a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java +++ b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java @@ -303,7 +303,7 @@ private static void ensureAttachmentSizeLimit( final SentryProfile profile = profileConverter.convertFromFile(traceFile.toPath()); profileChunk.setSentryProfile(profile); - } catch (IOException e) { + } catch (Exception e) { throw new SentryEnvelopeException("Profile conversion failed"); } } diff --git a/sentry/src/main/java/io/sentry/SentryTracer.java b/sentry/src/main/java/io/sentry/SentryTracer.java index 0496f407219..21d5088a181 100644 --- a/sentry/src/main/java/io/sentry/SentryTracer.java +++ b/sentry/src/main/java/io/sentry/SentryTracer.java @@ -83,8 +83,8 @@ public SentryTracer( setDefaultSpanData(root); - final @NotNull SentryId continuousProfilerId = - scopes.getOptions().getContinuousProfiler().getProfilerId(); + final @NotNull SentryId continuousProfilerId = getProfilerId(); + if (!continuousProfilerId.equals(SentryId.EMPTY_ID) && Boolean.TRUE.equals(isSampled())) { this.contexts.setProfile(new ProfileContext(continuousProfilerId)); } @@ -229,7 +229,7 @@ public void finish( } }); - // any un-finished childs will remain unfinished + // any un-finished children will remain unfinished // as relay takes care of setting the end-timestamp + deadline_exceeded // see // https://github.com/getsentry/relay/blob/40697d0a1c54e5e7ad8d183fc7f9543b94fe3839/relay-general/src/store/transactions/processor.rs#L374-L378 @@ -244,7 +244,8 @@ public void finish( .onTransactionFinish(this, performanceCollectionData.get(), scopes.getOptions()); } if (scopes.getOptions().isContinuousProfilingEnabled() - && scopes.getOptions().getProfileLifecycle() == ProfileLifecycle.TRACE) { + && scopes.getOptions().getProfileLifecycle() == ProfileLifecycle.TRACE + && root.getSpanContext().getProfilerId().equals(SentryId.EMPTY_ID)) { scopes.getOptions().getContinuousProfiler().stopProfiler(ProfileLifecycle.TRACE); } if (performanceCollectionData.get() != null) { @@ -543,8 +544,7 @@ 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(); + final @NotNull SentryId profilerId = getProfilerId(); if (!profilerId.equals(SentryId.EMPTY_ID) && Boolean.TRUE.equals(span.isSampled())) { span.setData(SpanDataConvention.PROFILER_ID, profilerId.toString()); } @@ -553,6 +553,12 @@ private void setDefaultSpanData(final @NotNull ISpan span) { span.setData(SpanDataConvention.THREAD_NAME, threadChecker.getCurrentThreadName()); } + private @NotNull SentryId getProfilerId() { + return !root.getSpanContext().getProfilerId().equals(SentryId.EMPTY_ID) + ? root.getSpanContext().getProfilerId() + : scopes.getOptions().getContinuousProfiler().getProfilerId(); + } + @Override public @NotNull ISpan startChild(final @NotNull String operation) { return this.startChild(operation, (String) null); diff --git a/sentry/src/main/java/io/sentry/SpanContext.java b/sentry/src/main/java/io/sentry/SpanContext.java index 2999ea4a2b8..8bfc83e6458 100644 --- a/sentry/src/main/java/io/sentry/SpanContext.java +++ b/sentry/src/main/java/io/sentry/SpanContext.java @@ -55,6 +55,12 @@ public class SpanContext implements JsonUnknown, JsonSerializable { protected @Nullable Baggage baggage; + /** + * Set the profiler id associated with this transaction. If set to a non-empty id, this value will + * be sent to sentry instead of {@link SentryOptions#getContinuousProfiler} + */ + private @NotNull SentryId profilerId = SentryId.EMPTY_ID; + public SpanContext( final @NotNull String operation, final @Nullable TracesSamplingDecision samplingDecision) { this(new SentryId(), new SpanId(), operation, null, samplingDecision); @@ -304,6 +310,14 @@ public int hashCode() { return Objects.hash(traceId, spanId, parentSpanId, op, description, getStatus()); } + public @NotNull SentryId getProfilerId() { + return profilerId; + } + + public void setProfilerId(@NotNull SentryId profilerId) { + this.profilerId = profilerId; + } + // region JsonSerializable public static final class JsonKeys { diff --git a/sentry/src/main/java/io/sentry/transport/RateLimiter.java b/sentry/src/main/java/io/sentry/transport/RateLimiter.java index eae6a23bf66..fc07e59c063 100644 --- a/sentry/src/main/java/io/sentry/transport/RateLimiter.java +++ b/sentry/src/main/java/io/sentry/transport/RateLimiter.java @@ -174,7 +174,12 @@ private void markHintWhenSendingFailed(final @NotNull Hint hint, final boolean r @SuppressWarnings({"JdkObsolete", "JavaUtilDate"}) private boolean isRetryAfter(final @NotNull String itemType) { final List dataCategory = getCategoryFromItemType(itemType); - return dataCategory.stream().anyMatch(this::isActiveForCategory); + for (DataCategory category : dataCategory) { + if (isActiveForCategory(category)) { + return true; + } + } + return false; } /**