Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
ddd9bed
feat: Add standalone app start transaction (happy path)
buenaflor Apr 2, 2026
fdd26df
feat: Add non-activity app start path with end time resolution
buenaflor Apr 2, 2026
e5b7a18
feat: Support non-activity app start tracing without bytecode instrum…
buenaflor Apr 2, 2026
11898dc
refactor: Consolidate non-activity app start time-span resolution
buenaflor Apr 23, 2026
5efab1d
chore(samples): Register TestBroadcastReceiver in manifest
buenaflor Apr 23, 2026
f6c070d
Merge remote-tracking branch 'origin/main' into feat/standalone-app-s…
buenaflor Apr 23, 2026
215a549
fix(app-start): resolve standalone tracing misclassification and dupl…
buenaflor Apr 24, 2026
50a2f41
fix(android): refine standalone app start tracing
buenaflor Apr 28, 2026
26a83cc
chore: Update generated files
buenaflor Apr 28, 2026
a60d966
style(core): Apply spotless formatting
buenaflor Apr 28, 2026
09bac52
changelog
buenaflor Apr 28, 2026
5387e76
Merge branch 'main' into feat/standalone-app-start-tracing
buenaflor Apr 28, 2026
f7c9c62
fix(android): Use stable app start transaction name
buenaflor May 4, 2026
9adfa01
feat(android): Add standalone app start tracing
buenaflor May 7, 2026
87ff0d6
fix(android): Handle non-activity app starts below API 24
buenaflor May 7, 2026
706adad
fix(android): Guard app start timestamp clock base
buenaflor May 11, 2026
9bf80c4
Merge branch 'main' into feat/standalone-app-start-tracing
buenaflor May 11, 2026
5d6174e
ref(android): Remove app start reason plumbing
buenaflor May 11, 2026
dfb5aea
Merge remote-tracking branch 'origin/feat/standalone-app-start-tracin…
buenaflor May 11, 2026
e8be515
ref(android): Clarify no-activity app start handling
buenaflor May 11, 2026
b5a6336
docs(android): Clarify non-activity app start fallback
buenaflor May 11, 2026
f3ef6ce
fix(android): Preserve legacy no-activity app start guard
buenaflor May 11, 2026
9da8369
test(android): Opt into standalone no-activity API 35 tests
buenaflor May 11, 2026
72268a3
fix(android): Schedule no-activity idle check when standalone listene…
buenaflor May 11, 2026
cbaebe2
ref(android): Remove dead foregroundImportance check in standalone ap…
buenaflor May 11, 2026
b1d738b
fix(android): Prevent duplicate standalone app start measurements
buenaflor May 11, 2026
9f5cac5
ref(android): Remove unused app start application context
buenaflor May 11, 2026
51c2bff
ref(android): Rename getAppStartTimeSpanDirect to getAppStartTimeSpan…
buenaflor May 12, 2026
b7fabc6
fix(android): Do not set TTID/TTFD contributing flags on standalone a…
buenaflor May 12, 2026
b871644
fix(android): Add volatile to noActivityStartedListener for cross-thr…
buenaflor May 12, 2026
e681be9
fix(android): Clear stale app start sampling decision in non-activity…
buenaflor May 12, 2026
cdc6178
fix: Format adb test commands in TestBroadcastReceiver JavaDoc
buenaflor May 12, 2026
38ac42c
ref(android): Rename headless app start handling
buenaflor May 18, 2026
d1262c2
fix(android): Align foreground app start measurements
buenaflor May 18, 2026
be261f6
fix(android): Gate headless app start end time
buenaflor May 18, 2026
41ebd25
test(android): Update API 35 headless app start expectation
buenaflor May 18, 2026
e0d714c
Fix headless app-start idle scheduling
buenaflor May 18, 2026
58dda40
ref(android): Clarify headless app start state names
buenaflor May 27, 2026
12719e6
Merge remote-tracking branch 'origin/main' into feat/standalone-app-s…
buenaflor Jun 1, 2026
2bc059f
ref(android): Use app.start origin for headless app start transaction
buenaflor Jun 1, 2026
cf623f3
ref(android): refine standalone app start trace continuation
buenaflor Jun 2, 2026
17a9c68
fix(android): align headless app start tests with uptime-based onCrea…
buenaflor Jun 2, 2026
467d0ce
test(android): add standalone app start E2E harness
buenaflor Jun 2, 2026
6e264d6
chore(android): remove standalone app start report
buenaflor Jun 2, 2026
e3ef325
test(android): clarify app start transaction shapes
buenaflor Jun 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

## Unreleased

### Features
Comment thread
buenaflor marked this conversation as resolved.

- Add `enableStandaloneAppStartTracing` option to send app start as a standalone transaction instead of attaching it as a child span of the first activity transaction ([#5342](https://github.com/getsentry/sentry-java/pull/5342))
- Disabled by default; opt in via `options.isEnableStandaloneAppStartTracing = true` or manifest meta-data `io.sentry.standalone-app-start-tracing.enable`
- Emits a transaction named `App Start` with op `app.start`, carrying the existing app start measurements and phase spans (`process.load`, `contentprovider.load`, `application.load`, activity lifecycle spans) as direct children of the root
- The standalone transaction shares the same `traceId` as the first `ui.load` activity transaction so they remain linked in the trace view
- Also covers non-activity starts (broadcast receivers, services, content providers)

### Fixes

- Session Replay: Populate `trace_ids` in replay events to enable searching replays by trace ID ([#5473](https://github.com/getsentry/sentry-java/pull/5473))
Expand Down
370 changes: 370 additions & 0 deletions scripts/test-standalone-app-start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,370 @@
#!/usr/bin/env bash
# End-to-end test harness for standalone app start tracing (issue #5046).
# Exercises scenarios 1a, 1c, 2a-2f against two running emulators.
# Requires: adb, two running emulators (API 36 + API 33), JDK 17.

set -u

REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$REPO_ROOT"

export JAVA_HOME="${JAVA_HOME:-/Library/Java/JavaVirtualMachines/openjdk-17.jdk/Contents/Home}"

PKG="io.sentry.samples.android"
APK_SRC="sentry-samples/sentry-samples-android/build/outputs/apk/debug/sentry-samples-android-debug.apk"
EMU_API36="emulator-5554"
EMU_API33="emulator-5556"
OUT_DIR="/tmp/standalone-app-start-logs"
APK_A="$OUT_DIR/APK-A.apk" # flag=on, simulate-plugin=on (tier 1)
APK_B="$OUT_DIR/APK-B.apk" # flag=off, simulate-plugin=off (regression)
APK_C="$OUT_DIR/APK-C.apk" # flag=on, simulate-plugin=off (tier 2 / tier 3)
MAIN_ACTIVITY="${PKG}/.MainActivity"
RECEIVER="${PKG}/.TestBroadcastReceiver"
BROADCAST_ACTION="io.sentry.samples.android.TEST_BROADCAST"
SERVICE="${PKG}/.DummyService"

# Wait durations (seconds). Must exceed the transaction deadline timeout (30s default)
# because the sample MainActivity keeps the main thread busy so idleTimeout doesn't fire.
WAIT_ACTIVITY=35
WAIT_BROADCAST=8
WAIT_COMBO=40
# Max time we wait for Sentry to actually flush cached envelopes to sentry.io after we
# foreground the app. Emulators often fail on IPv6 first and take ~60-75s before falling
# back to IPv4 on the transport's retry, so 120s gives comfortable slack.
DELIVERY_TIMEOUT=120

mkdir -p "$OUT_DIR"
rm -f "$OUT_DIR"/*.log

PASS=0
FAIL=0
FAIL_LINES=()

red() { printf "\033[31m%s\033[0m" "$*"; }
green() { printf "\033[32m%s\033[0m" "$*"; }
bold() { printf "\033[1m%s\033[0m" "$*"; }

banner() { echo ""; bold "========= $* ========="; echo ""; }

build_apk() {
local flag=$1 simulate=$2 dest=$3 label=$4
echo "--- build $label: -PstandaloneAppStart=$flag -PsimulateSentryGradlePlugin=$simulate ---"
./gradlew :sentry-samples:sentry-samples-android:assembleDebug \
-PstandaloneAppStart="$flag" \
-PsimulateSentryGradlePlugin="$simulate" \
-q 2>&1 | tail -3
if [[ ! -f "$APK_SRC" ]]; then
echo "BUILD FAILED: APK not produced"
exit 1
fi
cp "$APK_SRC" "$dest"
echo " saved -> $dest"
}

install_from() {
local device=$1 apk=$2
echo "--- install $(basename $apk) on $device ---"
adb -s "$device" install -r -t "$apk" 2>&1 | tail -1
}

wake_device() {
local device=$1
adb -s "$device" shell input keyevent KEYCODE_WAKEUP >/dev/null 2>&1 || true
adb -s "$device" shell input keyevent 82 >/dev/null 2>&1 || true
}

prep_for_run() {
local device=$1
adb -s "$device" shell am force-stop "$PKG"
adb -s "$device" logcat -c
# brief pause to let system settle
sleep 0.3
}

dump_log() {
local device=$1 file=$2
adb -s "$device" logcat -d -v time SentryE2E:D '*:S' > "$file"
# Also capture full Sentry SDK debug log for diagnostics.
adb -s "$device" logcat -d -v time Sentry:D SentryE2E:D StrictMode:D AndroidRuntime:E '*:S' \
> "${file%.log}.full.log"
}

# Extract traceIds of logged transactions in order.
extract_traceids() {
local file=$1
grep -oE 'traceId=[a-f0-9]+' "$file" | sed 's/traceId=//'
}

# Extract unique names of logged transactions in order.
extract_names() {
local file=$1
awk '/SentryE2E.*TXN\|/ {
match($0, /name=[^|]+/); n = substr($0, RSTART+5, RLENGTH-5); print n
}' "$file"
}

assert() {
local desc=$1 cmd=$2
if eval "$cmd"; then
echo " $(green PASS) $desc"
PASS=$((PASS+1))
else
echo " $(red FAIL) $desc"
FAIL=$((FAIL+1))
FAIL_LINES+=("$desc")
fi
}

# Post-scenario drain: foreground the app (via MainActivity) so AndroidConnectionStatusProvider
# flips from DISCONNECTED → CONNECTED (it reports null during broadcast-only cold starts),
# then poll logcat until at least one "Envelope sent successfully" appears. Safe to call after
# TXN assertions are done; the extra MainActivity launch emits its own ui.load but by then the
# scenario log was already captured.
verify_delivery() {
local device=$1 scenario=$2
adb -s "$device" shell am start -n "$MAIN_ACTIVITY" >/dev/null
local drain_log="$OUT_DIR/${scenario}.delivery.log"
local deadline=$((SECONDS + DELIVERY_TIMEOUT))
local sent=0
echo " polling for envelope delivery (timeout ${DELIVERY_TIMEOUT}s)..."
while (( SECONDS < deadline )); do
adb -s "$device" logcat -d -v time Sentry:D '*:S' > "$drain_log"
sent=$(grep -c 'Envelope sent successfully' "$drain_log" || true)
[[ "$sent" -gt 0 ]] && break
sleep 3
done
local queued
queued=$(grep -c 'Adding Envelope to offline storage' "$drain_log" || true)
assert "delivery: envelopes sent to Sentry (sent=$sent queued=$queued)" \
"[[ '$sent' -gt '0' ]]"
}

# ================= SCENARIOS =================

build_needed_variants() {
banner "Build variants"
[[ $needs_A -eq 1 ]] && build_apk true true "$APK_A" "APK-A (flag=on, simulate-plugin=on)"
[[ $needs_B -eq 1 ]] && build_apk false false "$APK_B" "APK-B (flag=off, simulate-plugin=off)"
[[ $needs_C -eq 1 ]] && build_apk true false "$APK_C" "APK-C (flag=on, simulate-plugin=off)"
}

scenario_1a() {
banner "1a: Cold + flag ON (launcher) — API 36 / APK-A"
install_from "$EMU_API36" "$APK_A"
wake_device "$EMU_API36"
prep_for_run "$EMU_API36"
adb -s "$EMU_API36" shell am start -n "$MAIN_ACTIVITY" >/dev/null
sleep $WAIT_ACTIVITY
local log="$OUT_DIR/1a.log"
dump_log "$EMU_API36" "$log"

assert "App Start standalone txn emitted" \
"grep -qE 'name=App Start\|op=app\.start\|' '$log'"
assert "MainActivity ui.load txn emitted" \
"grep -qE 'name=MainActivity.*op=ui\.load' '$log'"
assert "ui.load txn does NOT contain app.start.* child span" \
"! grep -E 'name=MainActivity.*op=ui\.load' '$log' | grep -qE 'children=.*app\.start\.(cold|warm)'"
assert "standalone txn has process.load or application.load child" \
"grep -E '\|op=app\.start\|' '$log' | grep -qE 'children=.*(process\.load|application\.load)'"
# Both txns share same traceId
local tids=$(extract_traceids "$log" | sort -u)
local count=$(echo "$tids" | wc -l | tr -d ' ')
assert "standalone + ui.load share traceId (unique traceId count = 1)" \
"[[ '$count' == '1' ]]"
verify_delivery "$EMU_API36" "1a"
}

scenario_1c() {
banner "1c: Cold + flag OFF (regression) — API 36 / APK-B"
install_from "$EMU_API36" "$APK_B"
wake_device "$EMU_API36"
prep_for_run "$EMU_API36"
adb -s "$EMU_API36" shell am start -n "$MAIN_ACTIVITY" >/dev/null
sleep $WAIT_ACTIVITY
local log="$OUT_DIR/1c.log"
dump_log "$EMU_API36" "$log"

assert "MainActivity ui.load txn emitted" \
"grep -qE 'name=MainActivity.*op=ui\.load' '$log'"
assert "ui.load txn CONTAINS app.start.* child span (legacy)" \
"grep -E 'name=MainActivity.*op=ui\.load' '$log' | grep -qE 'children=.*app\.start\.(cold|warm)'"
assert "NO standalone App Start txn emitted" \
"! grep -qE 'name=App Start\|op=app\.start\|' '$log'"
verify_delivery "$EMU_API36" "1c"
}

scenario_2a() {
banner "2a: Broadcast cold, tier 1 (simulated plugin) — API 36 / APK-A"
install_from "$EMU_API36" "$APK_A"
prep_for_run "$EMU_API36"
adb -s "$EMU_API36" shell am broadcast -a "$BROADCAST_ACTION" -n "$RECEIVER" >/dev/null
sleep $WAIT_BROADCAST
local log="$OUT_DIR/2a.log"
dump_log "$EMU_API36" "$log"

assert "App Start standalone txn emitted" \
"grep -qE 'name=App Start\|op=app\.start\|' '$log'"
assert "standalone has process.load child" \
"grep -qE '\|op=app\.start\|.*children=.*process\.load' '$log'"
assert "standalone has application.load child" \
"grep -qE '\|op=app\.start\|.*children=.*application\.load' '$log'"
assert "NO ui.load txn emitted" \
"! grep -qE 'op=ui\.load' '$log'"
verify_delivery "$EMU_API36" "2a"
}

scenario_2b() {
banner "2b: Broadcast cold, tier 2 (ApplicationStartInfo) — API 36 / APK-C"
install_from "$EMU_API36" "$APK_C"
prep_for_run "$EMU_API36"
adb -s "$EMU_API36" shell am broadcast -a "$BROADCAST_ACTION" -n "$RECEIVER" >/dev/null
sleep $WAIT_BROADCAST
local log="$OUT_DIR/2b.log"
dump_log "$EMU_API36" "$log"

assert "App Start standalone txn emitted (tier 2 API 35+)" \
"grep -qE 'name=App Start\|op=app\.start\|' '$log'"
assert "standalone has process.load child" \
"grep -qE '\|op=app\.start\|.*children=.*process\.load' '$log'"
assert "NO ui.load txn emitted" \
"! grep -qE 'op=ui\.load' '$log'"
verify_delivery "$EMU_API36" "2b"
}

scenario_2c() {
banner "2c: Broadcast cold, tier 3 (CLASS_LOADED fallback) — API 33 / APK-C"
install_from "$EMU_API33" "$APK_C"
prep_for_run "$EMU_API33"
adb -s "$EMU_API33" shell am broadcast -a "$BROADCAST_ACTION" -n "$RECEIVER" >/dev/null
sleep $WAIT_BROADCAST
local log="$OUT_DIR/2c.log"
dump_log "$EMU_API33" "$log"

assert "App Start standalone txn emitted (tier 3 fallback)" \
"grep -qE 'name=App Start\|op=app\.start\|' '$log'"
assert "standalone has process.load child" \
"grep -qE '\|op=app\.start\|.*children=.*process\.load' '$log'"
assert "NO ui.load txn emitted" \
"! grep -qE 'op=ui\.load' '$log'"
verify_delivery "$EMU_API33" "2c"
}

scenario_2d() {
banner "2d: Foreground service cold start — API 36 / APK-A"
install_from "$EMU_API36" "$APK_A"
# Notification permission is needed for the foreground service notification
adb -s "$EMU_API36" shell pm grant "$PKG" android.permission.POST_NOTIFICATIONS >/dev/null 2>&1 || true
prep_for_run "$EMU_API36"
adb -s "$EMU_API36" shell am start-foreground-service -n "$SERVICE" >/dev/null
sleep $WAIT_BROADCAST
local log="$OUT_DIR/2d.log"
dump_log "$EMU_API36" "$log"

assert "App Start standalone txn emitted via foreground service" \
"grep -qE 'name=App Start\|op=app\.start\|' '$log'"
assert "NO ui.load txn emitted" \
"! grep -qE 'op=ui\.load' '$log'"
verify_delivery "$EMU_API36" "2d"
}

scenario_2e() {
banner "2e: Broadcast → launcher (trace reuse) — API 36 / APK-A"
install_from "$EMU_API36" "$APK_A"
wake_device "$EMU_API36"
prep_for_run "$EMU_API36"
adb -s "$EMU_API36" shell am broadcast -a "$BROADCAST_ACTION" -n "$RECEIVER" >/dev/null
sleep 3
adb -s "$EMU_API36" shell am start -n "$MAIN_ACTIVITY" >/dev/null
sleep $WAIT_COMBO
local log="$OUT_DIR/2e.log"
dump_log "$EMU_API36" "$log"

assert "App Start standalone txn emitted (from broadcast)" \
"grep -qE 'name=App Start\|op=app\.start\|' '$log'"
assert "MainActivity ui.load txn emitted" \
"grep -qE 'name=MainActivity.*op=ui\.load' '$log'"
assert "ui.load has NO app.start.* child" \
"! grep -E 'name=MainActivity.*op=ui\.load' '$log' | grep -qE 'children=.*app\.start\.'"
local tids=$(extract_traceids "$log" | sort -u)
local count=$(echo "$tids" | wc -l | tr -d ' ')
assert "broadcast + launcher txns share same traceId" \
"[[ '$count' == '1' ]]"
verify_delivery "$EMU_API36" "2e"
}

scenario_2f() {
banner "2f: Broadcast + flag OFF (regression) — API 36 / APK-B"
install_from "$EMU_API36" "$APK_B"
prep_for_run "$EMU_API36"
adb -s "$EMU_API36" shell am broadcast -a "$BROADCAST_ACTION" -n "$RECEIVER" >/dev/null
sleep $WAIT_BROADCAST
local log="$OUT_DIR/2f.log"
dump_log "$EMU_API36" "$log"

assert "NO transactions emitted (flag off + no activity)" \
"! grep -qE 'SentryE2E.*TXN\\|' '$log'"
}

usage() {
cat <<EOF
Usage: $0 [scenario ...]

Without args, runs all 8 scenarios. With args, runs only those named.
Scenarios: 1a 1c 2a 2b 2c 2d 2e 2f

Variant needed per scenario:
1a,2a,2d,2e → APK-A (flag=on, simulate-plugin=on)
1c,2f → APK-B (flag=off, simulate-plugin=off)
2b,2c → APK-C (flag=on, simulate-plugin=off)

Examples:
$0 # all scenarios
$0 2c # just the tier-3 fallback case
$0 1a 2e # happy path + broadcast-then-launcher
EOF
}

# Determine which variants are needed based on requested scenarios.
needs_A=0; needs_B=0; needs_C=0
declare -a requested=()
if [[ $# -eq 0 ]]; then
requested=(1a 1c 2a 2b 2c 2d 2e 2f)
needs_A=1; needs_B=1; needs_C=1
else
for s in "$@"; do
case "$s" in
-h|--help) usage; exit 0 ;;
1a|2a|2d|2e) needs_A=1; requested+=("$s") ;;
1c|2f) needs_B=1; requested+=("$s") ;;
2b|2c) needs_C=1; requested+=("$s") ;;
*) echo "unknown scenario: $s"; usage; exit 2 ;;
esac
done
fi

# ================ RUN =================

banner "Pre-flight"
adb devices -l
echo ""
adb -s "$EMU_API36" shell getprop ro.build.version.sdk | awk '{print "emulator-5554 API: "$0}'
adb -s "$EMU_API33" shell getprop ro.build.version.sdk | awk '{print "emulator-5556 API: "$0}'

build_needed_variants
for s in "${requested[@]}"; do
"scenario_$s"
done

banner "SUMMARY"
echo " PASS: $(green $PASS)"
echo " FAIL: $(red $FAIL)"
if [[ $FAIL -gt 0 ]]; then
echo ""
echo " Failing assertions:"
for l in "${FAIL_LINES[@]}"; do
echo " - $l"
done
fi
echo ""
echo " Logs: $OUT_DIR"
exit $FAIL
Loading
Loading