From 1cb3cad2fc566dbbc7dbbf127d558e8d0e2772e6 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Mon, 15 Sep 2025 14:20:59 +0200 Subject: [PATCH 01/18] add skipProfiling flag to TransactionOptions to be able to skip profiling and handle cases where profiling has been started by otel --- .../sentry/opentelemetry/SentrySpanExporter.java | 1 + sentry/api/sentry.api | 2 ++ sentry/src/main/java/io/sentry/Scopes.java | 8 +++++--- sentry/src/main/java/io/sentry/SentryTracer.java | 5 +++-- .../main/java/io/sentry/TransactionOptions.java | 14 ++++++++++++++ 5 files changed, 25 insertions(+), 5 deletions(-) diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanExporter.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanExporter.java index 268b8231a81..580b6b3bc98 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanExporter.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanExporter.java @@ -331,6 +331,7 @@ private void transferSpanDetails( transactionOptions.setStartTimestamp(new SentryLongDate(span.getStartEpochNanos())); transactionOptions.setSpanFactory(new DefaultSpanFactory()); + transactionOptions.setSkipProfiling(true); ITransaction sentryTransaction = scopesToUse.startTransaction(transactionContext, transactionOptions); diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index fdf8d871ab3..d7ac084db53 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -4212,12 +4212,14 @@ public final class io/sentry/TransactionOptions : io/sentry/SpanOptions { public fun getTransactionFinishedCallback ()Lio/sentry/TransactionFinishedCallback; public fun isAppStartTransaction ()Z public fun isBindToScope ()Z + public fun isSkipProfiling ()Z public fun isWaitForChildren ()Z public fun setAppStartTransaction (Z)V public fun setBindToScope (Z)V public fun setCustomSamplingContext (Lio/sentry/CustomSamplingContext;)V public fun setDeadlineTimeout (Ljava/lang/Long;)V public fun setIdleTimeout (Ljava/lang/Long;)V + public fun setSkipProfiling (Z)V public fun setSpanFactory (Lio/sentry/ISpanFactory;)V public fun setTransactionFinishedCallback (Lio/sentry/TransactionFinishedCallback;)V public fun setWaitForChildren (Z)V diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index 491da449ca1..ca8a765a1b6 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -938,13 +938,15 @@ 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().getProfileLifecycle() == ProfileLifecycle.TRACE + && !transactionOptions.isSkipProfiling()) { getOptions() .getContinuousProfiler() .startProfiler(ProfileLifecycle.TRACE, getOptions().getInternalTracesSampler()); diff --git a/sentry/src/main/java/io/sentry/SentryTracer.java b/sentry/src/main/java/io/sentry/SentryTracer.java index 0496f407219..77303df9b6b 100644 --- a/sentry/src/main/java/io/sentry/SentryTracer.java +++ b/sentry/src/main/java/io/sentry/SentryTracer.java @@ -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 + && !transactionOptions.isSkipProfiling()) { scopes.getOptions().getContinuousProfiler().stopProfiler(ProfileLifecycle.TRACE); } if (performanceCollectionData.get() != null) { diff --git a/sentry/src/main/java/io/sentry/TransactionOptions.java b/sentry/src/main/java/io/sentry/TransactionOptions.java index 0da3cf611c2..8b3d0b04cdd 100644 --- a/sentry/src/main/java/io/sentry/TransactionOptions.java +++ b/sentry/src/main/java/io/sentry/TransactionOptions.java @@ -54,6 +54,12 @@ public final class TransactionOptions extends SpanOptions { /** Span factory to use. Uses factory configured in {@link SentryOptions} if `null`. */ @ApiStatus.Internal private @Nullable ISpanFactory spanFactory = null; + /** + * When set to `true` the transaction will not be profiled even if continuous profiling is enabled + * and ProfileLifecycle is set to TRACE in {@link SentryOptions}. + */ + private boolean skipProfiling = false; + /** * Gets the customSamplingContext * @@ -186,4 +192,12 @@ public boolean isAppStartTransaction() { public void setSpanFactory(final @NotNull ISpanFactory spanFactory) { this.spanFactory = spanFactory; } + + public boolean isSkipProfiling() { + return skipProfiling; + } + + public void setSkipProfiling(boolean skipProfiling) { + this.skipProfiling = skipProfiling; + } } From d9790e339b4568ddf50fc1bb885831212a7ca151 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 16 Sep 2025 16:51:22 +0200 Subject: [PATCH 02/18] add profilerId to spanContext so that otel span processor can propagate this to the exporter and SentryTracer --- .../opentelemetry/OtelSentrySpanProcessor.java | 3 +++ .../sentry/opentelemetry/SentrySpanExporter.java | 1 - sentry/src/main/java/io/sentry/Scopes.java | 15 +++++++-------- sentry/src/main/java/io/sentry/SentryTracer.java | 15 ++++++++++----- sentry/src/main/java/io/sentry/SpanContext.java | 14 ++++++++++++++ .../main/java/io/sentry/TransactionOptions.java | 14 -------------- 6 files changed, 34 insertions(+), 28 deletions(-) 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-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanExporter.java b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanExporter.java index 580b6b3bc98..268b8231a81 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanExporter.java +++ b/sentry-opentelemetry/sentry-opentelemetry-core/src/main/java/io/sentry/opentelemetry/SentrySpanExporter.java @@ -331,7 +331,6 @@ private void transferSpanDetails( transactionOptions.setStartTimestamp(new SentryLongDate(span.getStartEpochNanos())); transactionOptions.setSpanFactory(new DefaultSpanFactory()); - transactionOptions.setSkipProfiling(true); ITransaction sentryTransaction = scopesToUse.startTransaction(transactionContext, transactionOptions); diff --git a/sentry/src/main/java/io/sentry/Scopes.java b/sentry/src/main/java/io/sentry/Scopes.java index ca8a765a1b6..1b44d3d80f2 100644 --- a/sentry/src/main/java/io/sentry/Scopes.java +++ b/sentry/src/main/java/io/sentry/Scopes.java @@ -943,14 +943,13 @@ public void flush(long timeoutMillis) { // 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 - && !transactionOptions.isSkipProfiling()) { - 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/SentryTracer.java b/sentry/src/main/java/io/sentry/SentryTracer.java index 77303df9b6b..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)); } @@ -245,7 +245,7 @@ public void finish( } if (scopes.getOptions().isContinuousProfilingEnabled() && scopes.getOptions().getProfileLifecycle() == ProfileLifecycle.TRACE - && !transactionOptions.isSkipProfiling()) { + && root.getSpanContext().getProfilerId().equals(SentryId.EMPTY_ID)) { scopes.getOptions().getContinuousProfiler().stopProfiler(ProfileLifecycle.TRACE); } if (performanceCollectionData.get() != null) { @@ -544,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()); } @@ -554,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/TransactionOptions.java b/sentry/src/main/java/io/sentry/TransactionOptions.java index 8b3d0b04cdd..0da3cf611c2 100644 --- a/sentry/src/main/java/io/sentry/TransactionOptions.java +++ b/sentry/src/main/java/io/sentry/TransactionOptions.java @@ -54,12 +54,6 @@ public final class TransactionOptions extends SpanOptions { /** Span factory to use. Uses factory configured in {@link SentryOptions} if `null`. */ @ApiStatus.Internal private @Nullable ISpanFactory spanFactory = null; - /** - * When set to `true` the transaction will not be profiled even if continuous profiling is enabled - * and ProfileLifecycle is set to TRACE in {@link SentryOptions}. - */ - private boolean skipProfiling = false; - /** * Gets the customSamplingContext * @@ -192,12 +186,4 @@ public boolean isAppStartTransaction() { public void setSpanFactory(final @NotNull ISpanFactory spanFactory) { this.spanFactory = spanFactory; } - - public boolean isSkipProfiling() { - return skipProfiling; - } - - public void setSkipProfiling(boolean skipProfiling) { - this.skipProfiling = skipProfiling; - } } From b86145b54bbe92d0dbc56cebb4e01a303249f124 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Wed, 17 Sep 2025 08:13:20 +0200 Subject: [PATCH 03/18] immediately end profiling when stopProfiler is called --- .../profiling/JavaContinuousProfiler.java | 16 ++++++++-------- .../profiling/JavaContinuousProfilerTest.kt | 16 +++++----------- 2 files changed, 13 insertions(+), 19 deletions(-) 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..2742d6931ac 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 @@ -58,7 +58,6 @@ public final class JavaContinuousProfiler private @Nullable AsyncProfiler profiler; private volatile boolean shouldSample = true; - private boolean shouldStop = false; private boolean isSampled = false; private int rootSpanCounter = 0; @@ -166,7 +165,6 @@ public void startProfiler( } if (!isRunning()) { - shouldStop = false; logger.log(SentryLevel.DEBUG, "Started Profiler."); start(); } @@ -250,7 +248,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 +268,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; } } @@ -343,7 +344,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 { @@ -363,9 +364,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); } } 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..6873f26649a 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) } @@ -321,11 +318,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 +331,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 From 9b4d2e1f640a39a0dae73d1ffbcfb69c93085b44 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Wed, 17 Sep 2025 08:30:08 +0200 Subject: [PATCH 04/18] bump api, fix android api 24 code --- sentry/api/sentry.api | 4 ++-- sentry/src/main/java/io/sentry/transport/RateLimiter.java | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index d7ac084db53..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 @@ -4212,14 +4214,12 @@ public final class io/sentry/TransactionOptions : io/sentry/SpanOptions { public fun getTransactionFinishedCallback ()Lio/sentry/TransactionFinishedCallback; public fun isAppStartTransaction ()Z public fun isBindToScope ()Z - public fun isSkipProfiling ()Z public fun isWaitForChildren ()Z public fun setAppStartTransaction (Z)V public fun setBindToScope (Z)V public fun setCustomSamplingContext (Lio/sentry/CustomSamplingContext;)V public fun setDeadlineTimeout (Ljava/lang/Long;)V public fun setIdleTimeout (Ljava/lang/Long;)V - public fun setSkipProfiling (Z)V public fun setSpanFactory (Lio/sentry/ISpanFactory;)V public fun setTransactionFinishedCallback (Lio/sentry/TransactionFinishedCallback;)V public fun setWaitForChildren (Z)V 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; } /** From 732894fcf0d3300b64a58c0aacfed7ae7e04d5de Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Wed, 17 Sep 2025 08:32:01 +0200 Subject: [PATCH 05/18] catch all exception happening when converting from jfr --- sentry/src/main/java/io/sentry/SentryEnvelopeItem.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"); } } From cde0eb1ba6526e81266ff0c1c016540251cc1492 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Fri, 19 Sep 2025 10:32:19 +0200 Subject: [PATCH 06/18] simplify JavaContinuous profiler by catching AsyncProfiler instantiation exceptions in provider --- .../profiling/JavaContinuousProfiler.java | 32 +++---------------- ...yncProfilerContinuousProfilerProvider.java | 10 ++++++ 2 files changed, 15 insertions(+), 27 deletions(-) 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 2742d6931ac..dce502d720e 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,7 +56,7 @@ public final class JavaContinuousProfiler private @NotNull String filename = ""; - private @Nullable AsyncProfiler profiler; + private @NotNull AsyncProfiler profiler; private volatile boolean shouldSample = true; private boolean isSampled = false; private int rootSpanCounter = 0; @@ -68,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; @@ -76,19 +77,11 @@ public JavaContinuousProfiler( initializeProfiler(); } - private void initializeProfiler() { - try { + 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); - } catch (Exception e) { - logger.log( - SentryLevel.WARNING, - "Failed to initialize AsyncProfiler. Profiling will be disabled.", - e); - this.profiler = null; - } } private boolean init() { @@ -97,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, @@ -206,11 +194,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); @@ -294,11 +277,6 @@ 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) { @@ -406,7 +384,7 @@ private void sendChunks(final @NotNull IScopes scopes, final @NotNull SentryOpti @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..b1cc2c33795 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) { + 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(); + } } } From c694f0c2463b64763cf01073a1f2f5d66b993746 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Fri, 19 Sep 2025 10:35:19 +0200 Subject: [PATCH 07/18] add exists and writable info to log message --- .../asyncprofiler/profiling/JavaContinuousProfiler.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 dce502d720e..c2f6682a147 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 @@ -102,8 +102,11 @@ 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; } From 9f941dda51c151e2217e9d918a4340c06368b50d Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Fri, 19 Sep 2025 10:53:14 +0200 Subject: [PATCH 08/18] add method to safely delete file --- .../profiling/JavaContinuousProfiler.java | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) 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 c2f6682a147..bc62450d601 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 @@ -215,9 +215,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; } @@ -285,9 +283,7 @@ private void stop(final boolean restartProfiler) { } 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 @@ -312,9 +308,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 @@ -384,6 +378,16 @@ 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()) { From e45ee44515b03338d3f26d5d718700e5fc009605 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Fri, 19 Sep 2025 10:53:47 +0200 Subject: [PATCH 09/18] remove setNative call --- .../convert/JfrAsyncProfilerToSentryProfileConverter.java | 1 - 1 file changed, 1 deletion(-) 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..c22771d437e 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 @@ -167,7 +167,6 @@ private List createFramesAndCallStack(StackTrace stackTrace) { } SentryStackFrame frame = createStackFrame(element); - frame.setNative(isNativeFrame(types[i])); int frameIndex = getOrAddFrame(frame); callStack.add(frameIndex); } From 797f4681ffcdb091797a3b5f7a9baccae56aa648 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Mon, 22 Sep 2025 09:16:32 +0200 Subject: [PATCH 10/18] fix test --- ...yncProfilerToSentryProfileConverterTest.kt | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) 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 From b882020437b13357c46ebd252b144289b22645e9 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Mon, 22 Sep 2025 10:48:05 +0200 Subject: [PATCH 11/18] fix reference to commit we vendored from --- .../java/io/sentry/asyncprofiler/vendor/asyncprofiler/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From d9fc89e5c8164a9d7ca33ee8d2ad7fd6d6e05fb2 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 23 Sep 2025 09:47:49 +0200 Subject: [PATCH 12/18] drop event if it cannot be processed to not lose the whole chunk --- .../api/sentry-async-profiler.api | 2 +- ...AsyncProfilerToSentryProfileConverter.java | 29 ++++++++++++++----- 2 files changed, 22 insertions(+), 9 deletions(-) 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 c22771d437e..febd35d8e4b 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; @@ -27,13 +29,18 @@ public final class JfrAsyncProfilerToSentryProfileConverter extends JfrConverter 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 +67,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,15 +97,19 @@ public ProfileEventVisitor( @Override public void visit(Event event, long samples, long value) { - StackTrace stackTrace = jfr.stackTraces.get(event.stackTraceId); + 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); } } From ecc0215a3334b9ff93d21f1dc951ee828d2a4922 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Tue, 23 Sep 2025 08:15:58 +0000 Subject: [PATCH 13/18] Format code --- .../JfrAsyncProfilerToSentryProfileConverter.java | 2 +- .../profiling/JavaContinuousProfiler.java | 13 ++++++------- .../AsyncProfilerContinuousProfilerProvider.java | 4 ++-- 3 files changed, 9 insertions(+), 10 deletions(-) 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 febd35d8e4b..7cf240b1c2c 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 @@ -99,7 +99,7 @@ public ProfileEventVisitor( public void visit(Event event, long samples, long value) { try { StackTrace stackTrace = jfr.stackTraces.get(event.stackTraceId); - long threadId = resolveThreadId(event.tid); + long threadId = resolveThreadId(event.tid); if (stackTrace != null) { if (args.threads) { 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 bc62450d601..4df65dd69ee 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 @@ -78,10 +78,10 @@ public JavaContinuousProfiler( } 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); + 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() { @@ -105,8 +105,7 @@ private boolean init() { "Disabling profiling because traces directory is not writable or does not exist: %s (writable=%b, exists=%b)", profilingTracesDirPath, profileDir.canWrite(), - profileDir.exists() - ); + profileDir.exists()); return false; } @@ -384,7 +383,7 @@ private void safelyRemoveFile(File file) { file.delete(); } } catch (Exception e) { - logger.log(SentryLevel.INFO, "Failed to remove jfr file %s.", file.getAbsolutePath() , e); + logger.log(SentryLevel.INFO, "Failed to remove jfr file %s.", file.getAbsolutePath(), e); } } 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 b1cc2c33795..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 @@ -25,8 +25,8 @@ public final class AsyncProfilerContinuousProfilerProvider int profilingTracesHz, ISentryExecutorService executorService) { try { - return new JavaContinuousProfiler( - logger, profilingTracesDirPath, profilingTracesHz, executorService); + return new JavaContinuousProfiler( + logger, profilingTracesDirPath, profilingTracesHz, executorService); } catch (Exception e) { logger.log( SentryLevel.WARNING, From ab9b9502c396b044488fb295652596e5b4073b8e Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 23 Sep 2025 14:34:49 +0200 Subject: [PATCH 14/18] fix test --- .../asyncprofiler/profiling/JavaContinuousProfilerTest.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 6873f26649a..0b74404e76a 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 @@ -276,8 +276,10 @@ 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), ) } From 16ad3ac0bd91adda86440624fa95b63c3e16bb55 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Tue, 23 Sep 2025 12:38:12 +0000 Subject: [PATCH 15/18] Format code --- .../asyncprofiler/profiling/JavaContinuousProfilerTest.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 0b74404e76a..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 @@ -276,7 +276,9 @@ class JavaContinuousProfilerTest { verify(fixture.mockLogger) .log( eq(SentryLevel.WARNING), - eq("Disabling profiling because traces directory is not writable or does not exist: %s (writable=%b, exists=%b)"), + eq( + "Disabling profiling because traces directory is not writable or does not exist: %s (writable=%b, exists=%b)" + ), eq(expectedPath), eq(false), eq(true), From eb6a942e788a6f36eaf48286644b674387904eea Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 23 Sep 2025 14:42:34 +0200 Subject: [PATCH 16/18] fix test --- .../asyncprofiler/profiling/JavaContinuousProfilerTest.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 0b74404e76a..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 @@ -276,7 +276,9 @@ class JavaContinuousProfilerTest { verify(fixture.mockLogger) .log( eq(SentryLevel.WARNING), - eq("Disabling profiling because traces directory is not writable or does not exist: %s (writable=%b, exists=%b)"), + eq( + "Disabling profiling because traces directory is not writable or does not exist: %s (writable=%b, exists=%b)" + ), eq(expectedPath), eq(false), eq(true), From 0b1ea35f6c79cadb10994559f896cb2616404253 Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 23 Sep 2025 14:42:47 +0200 Subject: [PATCH 17/18] catch exceptions in startProfiler/stopProfiler --- .../asyncprofiler/profiling/JavaContinuousProfiler.java | 4 ++++ 1 file changed, 4 insertions(+) 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 4df65dd69ee..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 @@ -158,6 +158,8 @@ public void startProfiler( logger.log(SentryLevel.DEBUG, "Started Profiler."); start(); } + } catch (Exception e) { + logger.log(SentryLevel.ERROR, "Error starting profiler: ", e); } } @@ -326,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); } } From 2295cba6d8e44fb6d97bd8c1a1bf3cfb2f1657ad Mon Sep 17 00:00:00 2001 From: Lukas Bloder Date: Tue, 23 Sep 2025 16:49:26 +0200 Subject: [PATCH 18/18] fallback to threadId -1 if it cannot be resolved --- .../JfrAsyncProfilerToSentryProfileConverter.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) 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 7cf240b1c2c..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 @@ -26,6 +26,7 @@ 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; @@ -113,13 +114,16 @@ public void visit(Event event, long samples, long value) { } } - 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()