Skip to content

Commit e8ac53c

Browse files
authored
Merge branch 'main' into feat/opentelemetry-bom
2 parents 7177dc0 + cc59f48 commit e8ac53c

22 files changed

Lines changed: 1386 additions & 29 deletions

File tree

.cursor/rules/coding.mdc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ sentry-java is the Java and Android SDK for Sentry. This repository contains the
3131

3232
1. Follow existing code style and language
3333
2. Do not modify the API files (e.g. sentry.api) manually, instead run `./gradlew apiDump` to regenerate them
34-
3. Write comprehensive tests
34+
3. Write comprehensive tests. For assertions in new unit tests, prefer Google Truth (`com.google.common.truth.Truth.assertThat`) over `kotlin.test`/JUnit assertions; keep `kotlin.test` for test structure like `@Test` and `assertFailsWith`. Add `testImplementation(libs.google.truth)` to a module's `build.gradle.kts` if it isn't already present.
3535
4. New features should always be opt-in by default, extend `SentryOptions` or similar Option classes with getters and setters to enable/disable a new feature
3636
5. Consider backwards compatibility
3737

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,8 @@ The repository is organized into multiple modules:
146146
- Write comprehensive unit tests for new features
147147
- Android modules require both unit tests and instrumented tests where applicable
148148
- System tests validate end-to-end functionality with sample applications
149+
- **Assertions**: For new unit tests, prefer [Google Truth](https://truth.dev/) (`com.google.common.truth.Truth.assertThat`) over `kotlin.test`/JUnit assertions for its readable, fluent API. Keep using `kotlin.test` for test structure (`@Test`, `assertFailsWith`). See `sentry/src/test/java/io/sentry/DsnTest.kt` for the style. Don't rewrite existing `kotlin.test` assertions solely to switch libraries.
150+
- Truth is wired into the `sentry` module. When adding Truth-based tests to another module, add `testImplementation(libs.google.truth)` to that module's `build.gradle.kts`.
149151

150152
### Contributing Guidelines
151153
1. Follow existing code style and language

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,19 @@
2727
<scope>import</scope>
2828
</dependency>
2929
```
30+
- Add `Sentry.extendAppStart()`, `Sentry.finishExtendedAppStart()`, and `Sentry.getExtendedAppStartSpan()` to extend the app start measurement past the first frame for extra launch-time work on Android ([#5604](https://github.com/getsentry/sentry-java/pull/5604))
31+
- Requires standalone app start tracing (`options.isEnableStandaloneAppStartTracing`). Call `extendAppStart()` in `Application.onCreate` after SDK init and `finishExtendedAppStart()` when done:
32+
33+
```kotlin
34+
Sentry.extendAppStart()
35+
36+
// Optionally, retrieve the extended app start span to attach your own child spans
37+
val child = Sentry.getExtendedAppStartSpan()?.startChild("preload", "Preload resources")
38+
// ... extra launch-time work ...
39+
child?.finish()
40+
41+
Sentry.finishExtendedAppStart()
42+
```
3043
- Add `trace_metric_byte` data category and record byte-level client reports when trace metrics are discarded ([#5626](https://github.com/getsentry/sentry-java/pull/5626))
3144
- Support the `io.sentry.tombstone.report-historical` manifest option to enable historical tombstone reporting via `AndroidManifest.xml` `<meta-data>` ([#5683](https://github.com/getsentry/sentry-java/pull/5683))
3245

sentry-android-core/api/sentry-android-core.api

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,30 @@ public final class io/sentry/android/core/AppLifecycleIntegration : io/sentry/In
184184
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
185185
}
186186

187+
public final class io/sentry/android/core/AppStartExtension : io/sentry/IAppStartExtender {
188+
public fun <init> (Lio/sentry/android/core/performance/AppStartMetrics;)V
189+
public fun clear ()V
190+
public fun extendAppStart ()V
191+
public fun finishExtendedAppStart ()V
192+
public fun finishTransaction (Lio/sentry/SentryDate;)V
193+
public fun getExtendedAppStartSpan ()Lio/sentry/ISpan;
194+
public fun getExtendedEndTime ()Lio/sentry/SentryDate;
195+
public fun isActive ()Z
196+
public fun isExtended ()Z
197+
public fun setData (Ljava/lang/String;Ljava/lang/Object;)V
198+
public fun setExtendAppStartListener (Lio/sentry/android/core/AppStartExtension$ExtendAppStartListener;)V
199+
}
200+
201+
public abstract interface class io/sentry/android/core/AppStartExtension$ExtendAppStartListener {
202+
public abstract fun onExtendAppStartRequested ()Lio/sentry/android/core/AppStartExtension$ExtendedAppStart;
203+
}
204+
205+
public final class io/sentry/android/core/AppStartExtension$ExtendedAppStart {
206+
public final field span Lio/sentry/ISpan;
207+
public final field transaction Lio/sentry/ITransaction;
208+
public fun <init> (Lio/sentry/ITransaction;Lio/sentry/ISpan;)V
209+
}
210+
187211
public final class io/sentry/android/core/AppState : java/io/Closeable {
188212
public fun addAppStateListener (Lio/sentry/android/core/AppState$AppStateListener;)V
189213
public fun close ()V
@@ -739,12 +763,14 @@ public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/andr
739763
public static final field staticLock Lio/sentry/util/AutoClosableReentrantLock;
740764
public fun <init> ()V
741765
public fun addActivityLifecycleTimeSpans (Lio/sentry/android/core/performance/ActivityLifecycleTimeSpan;)V
766+
public fun canExtendAppStart ()Z
742767
public fun clear ()V
743768
public fun createProcessInitSpan ()Lio/sentry/android/core/performance/TimeSpan;
744769
public fun getActivityLifecycleTimeSpans ()Ljava/util/List;
745770
public fun getAppStartBaggageHeader ()Ljava/lang/String;
746771
public fun getAppStartContinuousProfiler ()Lio/sentry/IContinuousProfiler;
747772
public fun getAppStartEndTime ()Lio/sentry/SentryDate;
773+
public fun getAppStartExtension ()Lio/sentry/android/core/AppStartExtension;
748774
public fun getAppStartProfiler ()Lio/sentry/ITransactionProfiler;
749775
public fun getAppStartReason ()Ljava/lang/String;
750776
public fun getAppStartSamplingDecision ()Lio/sentry/TracesSamplingDecision;

sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java

Lines changed: 126 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ public final class ActivityLifecycleIntegration
6565
static final String APP_START_COLD = "app.start.cold";
6666
static final String TTID_OP = "ui.load.initial_display";
6767
static final String TTFD_OP = "ui.load.full_display";
68+
static final String APP_START_EXTENDED_OP = "app.start.extended";
69+
static final String APP_START_EXTENDED_DESC = "Extended App Start";
6870
static final long TTFD_TIMEOUT_MILLIS = 25000;
6971
// If a headless app start and the following activity's ui.load are more than this far apart, they
7072
// are treated as unrelated and not connected into the same trace.
@@ -139,7 +141,9 @@ public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions
139141
application.registerActivityLifecycleCallbacks(this);
140142

141143
if (performanceEnabled && this.options.isEnableStandaloneAppStartTracing()) {
142-
AppStartMetrics.getInstance().setHeadlessAppStartListener(this::onHeadlessAppStart);
144+
final @NotNull AppStartMetrics metrics = AppStartMetrics.getInstance();
145+
metrics.setHeadlessAppStartListener(this::onHeadlessAppStart);
146+
metrics.getAppStartExtension().setExtendAppStartListener(this::onExtendAppStartRequested);
143147
addIntegrationToSdkVersion("StandaloneAppStart");
144148
}
145149

@@ -154,7 +158,9 @@ private boolean isPerformanceEnabled(final @NotNull SentryAndroidOptions options
154158
@Override
155159
public void close() throws IOException {
156160
application.unregisterActivityLifecycleCallbacks(this);
157-
AppStartMetrics.getInstance().setHeadlessAppStartListener(null);
161+
final @NotNull AppStartMetrics metrics = AppStartMetrics.getInstance();
162+
metrics.setHeadlessAppStartListener(null);
163+
metrics.getAppStartExtension().setExtendAppStartListener(null);
158164

159165
if (options != null) {
160166
options.getLogger().log(SentryLevel.DEBUG, "ActivityLifecycleIntegration removed.");
@@ -259,17 +265,23 @@ private void startTracing(final @NotNull Activity activity) {
259265
transactionOptions.setAppStartTransaction(appStartSamplingDecision != null);
260266
setSpanOrigin(transactionOptions);
261267

268+
// Guards the headless-start check below with !isExtensionActive so the eager extension's
269+
// stored trace id isn't mistaken for a finished headless start.
270+
final boolean isExtensionActive =
271+
AppStartMetrics.getInstance().getAppStartExtension().isActive();
272+
262273
final @Nullable SentryId storedAppStartTraceId =
263274
AppStartMetrics.getInstance().getAppStartTraceId();
264-
final boolean isFollowingHeadlessAppStart = (storedAppStartTraceId != null);
275+
final boolean isFollowingHeadlessAppStart =
276+
!isExtensionActive && (storedAppStartTraceId != null);
265277

266278
final boolean isAppStart =
267279
!(firstActivityCreated || appStartTime == null || coldStart == null);
268-
// Foreground starts create app.start first; ui.load then shares its trace.
269280
final boolean createStandaloneAppStart =
270281
isAppStart
271282
&& options.isEnableStandaloneAppStartTracing()
272-
&& !isFollowingHeadlessAppStart;
283+
&& !isFollowingHeadlessAppStart
284+
&& !isExtensionActive;
273285

274286
if (createStandaloneAppStart) {
275287
final TransactionOptions appStartTransactionOptions = new TransactionOptions();
@@ -300,15 +312,23 @@ private void startTracing(final @NotNull Activity activity) {
300312
continueSentryTrace = appStartTransaction.toSentryTrace().getValue();
301313
final @Nullable BaggageHeader baggageHeader = appStartTransaction.toBaggageHeader(null);
302314
continueBaggage = baggageHeader == null ? null : baggageHeader.getValue();
303-
} else if (isFollowingHeadlessAppStart
304-
&& isWithinAppStartContinuationWindow(ttidStartTime)) {
315+
} else if (isExtensionActive
316+
|| (isFollowingHeadlessAppStart && isWithinAppStartContinuationWindow(ttidStartTime))) {
305317
continueSentryTrace = AppStartMetrics.getInstance().getAppStartSentryTraceHeader();
306318
continueBaggage = AppStartMetrics.getInstance().getAppStartBaggageHeader();
307319
} else {
308320
continueSentryTrace = null;
309321
continueBaggage = null;
310322
}
311323

324+
if (isExtensionActive && isAppStart) {
325+
// Only the launch activity sets the screen, so a later activity can't overwrite it. A
326+
// screen also keeps the processor from classifying the eager app.start as headless.
327+
AppStartMetrics.getInstance()
328+
.getAppStartExtension()
329+
.setData(APP_START_SCREEN_DATA, activityName);
330+
}
331+
312332
final @Nullable TransactionContext continuedContext =
313333
continueSentryTrace == null
314334
? null
@@ -328,8 +348,8 @@ && isWithinAppStartContinuationWindow(ttidStartTime)) {
328348
transactionOptions);
329349
}
330350

331-
if (isFollowingHeadlessAppStart) {
332-
// Consume the stored headless app-start trace so it isn't reused by another activity.
351+
if (isFollowingHeadlessAppStart || isExtensionActive) {
352+
// Consume the stored app-start trace so a later activity doesn't reuse it.
333353
AppStartMetrics.getInstance().setAppStartTraceId(null);
334354
AppStartMetrics.getInstance().setAppStartSentryTraceHeader(null);
335355
AppStartMetrics.getInstance().setAppStartBaggageHeader(null);
@@ -967,6 +987,9 @@ private void finishAppStartSpan(final @Nullable SentryDate endDate) {
967987
if (appStartTransaction != null && !appStartTransaction.isFinished()) {
968988
appStartTransaction.finish(SpanStatus.OK, appStartEndTime);
969989
}
990+
// Finish the eager extended transaction at the natural first-frame end. waitForChildren keeps
991+
// it open until the extended span finishes; no-op if the app start was not extended.
992+
AppStartMetrics.getInstance().getAppStartExtension().finishTransaction(appStartEndTime);
970993
}
971994
}
972995

@@ -994,17 +1017,60 @@ private void onHeadlessAppStart() {
9941017
return;
9951018
}
9961019

1020+
// Persist the end time so a later ui.load can tell whether it is close enough to continue this
1021+
// trace; without it the continuation window is unbounded.
1022+
metrics.setAppStartEndTime(endTime);
1023+
1024+
final @NotNull AppStartExtension extension = metrics.getAppStartExtension();
1025+
if (extension.isActive()) {
1026+
extension.finishTransaction(endTime);
1027+
return;
1028+
}
1029+
if (!metrics.shouldSendStartMeasurements(true)) {
1030+
return;
1031+
}
1032+
1033+
final @NotNull ITransaction transaction =
1034+
createStandaloneAppStartTransaction(startTime, null, false);
1035+
transaction.finish(SpanStatus.OK, endTime);
1036+
}
1037+
1038+
/**
1039+
* Creates the standalone {@code app.start} transaction (not bound to the scope) and persists its
1040+
* trace headers so a later {@code ui.load} can share the same trace. Shared by the headless path
1041+
* and the eager extension path. When {@code holdOpenForExtension} is true, the transaction waits
1042+
* for its children and gets a deadline so it stays open until the extended span finishes.
1043+
*/
1044+
private @NotNull ITransaction createStandaloneAppStartTransaction(
1045+
final @NotNull SentryDate startTime,
1046+
final @Nullable TracesSamplingDecision samplingDecision,
1047+
final boolean holdOpenForExtension) {
1048+
final @NotNull AppStartMetrics metrics = AppStartMetrics.getInstance();
1049+
9971050
final TransactionOptions txnOptions = new TransactionOptions();
9981051
txnOptions.setBindToScope(false);
9991052
txnOptions.setStartTimestamp(startTime);
10001053
txnOptions.setOrigin(APP_START_TRACE_ORIGIN);
1054+
txnOptions.setAppStartTransaction(samplingDecision != null);
1055+
if (holdOpenForExtension) {
1056+
txnOptions.setWaitForChildren(true);
1057+
final long deadlineTimeoutMillis = options.getDeadlineTimeout();
1058+
txnOptions.setDeadlineTimeout(deadlineTimeoutMillis <= 0 ? null : deadlineTimeoutMillis);
1059+
// Persist the end time (covering every finish path: user finish, first frame, deadline) so a
1060+
// later ui.load can tell whether it is close enough to continue this trace; without it the
1061+
// continuation window is unbounded.
1062+
txnOptions.setTransactionFinishedCallback(
1063+
finishedTransaction ->
1064+
AppStartMetrics.getInstance()
1065+
.setAppStartEndTime(finishedTransaction.getFinishDate()));
1066+
}
10011067

10021068
final @NotNull TransactionContext txnContext =
10031069
new TransactionContext(
10041070
STANDALONE_APP_START_NAME,
10051071
TransactionNameSource.COMPONENT,
10061072
STANDALONE_APP_START_OP,
1007-
null);
1073+
samplingDecision);
10081074

10091075
final @NotNull ITransaction transaction = scopes.startTransaction(txnContext, txnOptions);
10101076
final @Nullable String appStartReason = metrics.getAppStartReason();
@@ -1016,10 +1082,56 @@ private void onHeadlessAppStart() {
10161082
metrics.setAppStartSentryTraceHeader(transaction.toSentryTrace().getValue());
10171083
final @Nullable BaggageHeader baggageHeader = transaction.toBaggageHeader(null);
10181084
metrics.setAppStartBaggageHeader(baggageHeader == null ? null : baggageHeader.getValue());
1019-
// Persist the end time so a later activity can decide whether its ui.load is close enough in
1020-
// time to continue this trace.
1021-
metrics.setAppStartEndTime(endTime);
1085+
return transaction;
1086+
}
10221087

1023-
transaction.finish(SpanStatus.OK, endTime);
1088+
/**
1089+
* Handles {@code Sentry.extendAppStart()}: eagerly creates the standalone app.start transaction
1090+
* and the extended child span (we have scopes here), then hands both to the {@link
1091+
* AppStartExtension}, which owns them. The transaction is held open ({@code waitForChildren})
1092+
* until the user calls {@code Sentry.finishExtendedAppStart()} or the deadline forces it.
1093+
* Standalone-only: this is only registered as a listener when standalone app start tracing is
1094+
* enabled.
1095+
*/
1096+
private @Nullable AppStartExtension.ExtendedAppStart onExtendAppStartRequested() {
1097+
if (scopes == null
1098+
|| options == null
1099+
|| !performanceEnabled
1100+
|| !options.isEnableStandaloneAppStartTracing()) {
1101+
return null;
1102+
}
1103+
final @NotNull AppStartMetrics metrics = AppStartMetrics.getInstance();
1104+
1105+
final @NotNull TimeSpan appStartTimeSpan =
1106+
metrics.getAppStartTimeSpan().hasStarted()
1107+
? metrics.getAppStartTimeSpan()
1108+
: metrics.getSdkInitTimeSpan();
1109+
final @Nullable SentryDate startTime = appStartTimeSpan.getStartTimestamp();
1110+
if (startTime == null) {
1111+
return null;
1112+
}
1113+
1114+
// The app start sampling decision was pre-rolled on the previous run so the app start
1115+
// profiler could start before Sentry.init. It forces the trace sampling of the eager
1116+
// app.start transaction created below (no re-roll, staying consistent with whether the
1117+
// profiler actually started) and lets it bind the app start profiler. It's single-use:
1118+
// we clear it so the first ui.load can't also claim it.
1119+
final @Nullable TracesSamplingDecision samplingDecision = metrics.getAppStartSamplingDecision();
1120+
metrics.setAppStartSamplingDecision(null);
1121+
1122+
final @NotNull ITransaction transaction =
1123+
createStandaloneAppStartTransaction(startTime, samplingDecision, true);
1124+
1125+
final SpanOptions spanOptions = new SpanOptions();
1126+
setSpanOrigin(spanOptions);
1127+
final @NotNull ISpan extendedSpan =
1128+
transaction.startChild(
1129+
APP_START_EXTENDED_OP,
1130+
APP_START_EXTENDED_DESC,
1131+
AndroidDateUtils.getCurrentSentryDateTime(),
1132+
Instrumenter.SENTRY,
1133+
spanOptions);
1134+
1135+
return new AppStartExtension.ExtendedAppStart(transaction, extendedSpan);
10241136
}
10251137
}

sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ static void initializeIntegrationsAndProcessors(
198198
}
199199

200200
final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance();
201+
options.setAppStartExtender(appStartMetrics.getAppStartExtension());
201202

202203
if (options.getModulesLoader() instanceof NoOpModulesLoader) {
203204
options.setModulesLoader(new AssetsModulesLoader(context, options));

0 commit comments

Comments
 (0)