diff --git a/.agents/skills b/.agents/skills
new file mode 120000
index 00000000000..454b8427cd7
--- /dev/null
+++ b/.agents/skills
@@ -0,0 +1 @@
+../.claude/skills
\ No newline at end of file
diff --git a/.claude/settings.json b/.claude/settings.json
new file mode 100644
index 00000000000..ac6b69b1435
--- /dev/null
+++ b/.claude/settings.json
@@ -0,0 +1,34 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(find:*)",
+ "Bash(ls:*)",
+ "Bash(git:*)",
+ "Bash(git status:*)",
+ "Bash(git log:*)",
+ "Bash(git diff:*)",
+ "Bash(git show:*)",
+ "Bash(git branch:*)",
+ "Bash(git remote:*)",
+ "Bash(git tag:*)",
+ "Bash(git stash list:*)",
+ "Bash(git rev-parse:*)",
+ "Bash(gh pr view:*)",
+ "Bash(gh pr list:*)",
+ "Bash(gh pr checks:*)",
+ "Bash(gh pr diff:*)",
+ "Bash(gh issue view:*)",
+ "Bash(gh issue list:*)",
+ "Bash(gh run view:*)",
+ "Bash(gh run list:*)",
+ "Bash(gh run logs:*)",
+ "Bash(gh repo view:*)",
+ "WebFetch(domain:github.com)",
+ "WebFetch(domain:docs.sentry.io)",
+ "WebFetch(domain:develop.sentry.dev)",
+ "Bash(grep:*)",
+ "Bash(mv:*)"
+ ],
+ "deny": []
+ }
+}
diff --git a/.claude/skills/.gitignore b/.claude/skills/.gitignore
new file mode 100644
index 00000000000..2dd55eba801
--- /dev/null
+++ b/.claude/skills/.gitignore
@@ -0,0 +1,12 @@
+# Ignore dotagents-managed skills (synced from agents.toml)
+*
+# Keep custom repo-specific skills
+!.gitignore
+!create-java-pr/
+!create-java-pr/**
+!test/
+!test/**
+!btrace-perfetto/
+!btrace-perfetto/**
+!check-code-attribution/
+!check-code-attribution/**
diff --git a/.claude/skills/btrace-perfetto/SKILL.md b/.claude/skills/btrace-perfetto/SKILL.md
new file mode 100644
index 00000000000..8d9e5a6bca1
--- /dev/null
+++ b/.claude/skills/btrace-perfetto/SKILL.md
@@ -0,0 +1,303 @@
+---
+name: btrace-perfetto
+description: Capture and compare Perfetto traces using btrace 3.0 on an Android device. Use when asked to "profile", "capture trace", "perfetto trace", "btrace", "compare traces", "record perfetto", "trace touch events", "measure performance on device", or benchmark Android SDK changes between branches.
+allowed-tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, AskUserQuestion
+argument-hint: "[branch1] [branch2] [duration] [sql-query]"
+---
+
+# btrace Perfetto Trace Capture
+
+Capture Perfetto traces with btrace 3.0 on a connected Android device, optionally comparing two branches. Opens results in Perfetto UI with a prefilled SQL query. After capture, query traces locally with `trace_processor` to compute comparison stats.
+
+## Prerequisites
+
+Before starting, verify:
+
+1. **Connected device**: `adb devices` shows a device (Android 8.0+, 64-bit)
+2. **btrace CLI jar**: Check if `tools/btrace/rhea-trace-shell.jar` exists. If not, download it:
+ ```bash
+ mkdir -p tools/btrace/traces
+ curl -sL "https://repo1.maven.org/maven2/com/bytedance/btrace/rhea-trace-processor/3.0.0/rhea-trace-processor-3.0.0.jar" \
+ -o tools/btrace/rhea-trace-shell.jar
+ ```
+3. **Perfetto trace_processor**: Check if `/tmp/trace_processor` exists. If not, download it:
+ ```bash
+ # Download trace_processor (--fail ensures HTTP errors don't leave a file behind)
+ curl -sSL --fail "https://get.perfetto.dev/trace_processor" -o /tmp/trace_processor
+
+ # Verify magic bytes directly — file(1) output is too inconsistent across
+ # versions/platforms to rely on for scripts or PIE binaries.
+ magic=$(head -c 4 /tmp/trace_processor 2>/dev/null | od -An -vtx1 -N4 | tr -d ' \n')
+ case "$magic" in
+ 2321*) ;; # #! shebang (script)
+ 7f454c46) ;; # ELF (Linux)
+ cffaedfe|cefaedfe|feedfacf|feedface) ;; # Mach-O (macOS)
+ cafebabe) ;; # Mach-O universal
+ *)
+ echo "Error: Downloaded file is not a valid script or executable (magic: ${magic:-empty})"
+ rm -f /tmp/trace_processor
+ exit 1
+ ;;
+ esac
+
+ # Make executable only after verification
+ chmod +x /tmp/trace_processor
+ ```
+4. **Device ABI**: Run `adb shell getprop ro.product.cpu.abi` — btrace only supports arm64-v8a and armeabi-v7a (no x86/x86_64)
+
+## Step 1: Parse Arguments
+
+| Argument | Default | Description |
+|----------|---------|-------------|
+| branch1 | current branch | First branch to trace |
+| branch2 | `main` | Second branch to compare against |
+| duration | `30` | Trace duration in seconds |
+| sql-query | see below | SQL query to prefill in Perfetto UI |
+
+If no arguments are provided, ask the user what they want to trace and which branches to compare. If only one branch is given, capture only that branch (no comparison).
+
+## Step 2: Integrate btrace into Sample App
+
+The sample app is at `sentry-samples/sentry-samples-android/`.
+
+### 2a: Add btrace dependency
+
+In `sentry-samples/sentry-samples-android/build.gradle.kts`, add to the `dependencies` block:
+
+```kotlin
+implementation("com.bytedance.btrace:rhea-inhouse:3.0.0")
+```
+
+### 2b: Restrict ABI to device architecture
+
+The btrace native library (shadowhook) does not support x86/x86_64. Replace the `ndk` abiFilters line in `defaultConfig` to match the connected device:
+
+```kotlin
+ndk { abiFilters.addAll(listOf("arm64-v8a")) }
+```
+
+Adjust if the device reports a different ABI.
+
+### 2c: Initialize btrace in Application
+
+In `MyApplication.java`, add `attachBaseContext`:
+
+```java
+import android.content.Context;
+import com.bytedance.rheatrace.RheaTrace3;
+
+// Add before onCreate:
+@Override
+protected void attachBaseContext(Context base) {
+ super.attachBaseContext(base);
+ RheaTrace3.init(base);
+}
+```
+
+**Important**: The package is `com.bytedance.rheatrace`, not `com.bytedance.btrace`.
+
+### 2d: Add ProGuard keep rules (release builds only)
+
+Only needed when building release. In `sentry-samples/sentry-samples-android/proguard-rules.pro`, add:
+
+```
+-keep class com.bytedance.rheatrace.** { *; }
+-keepnames class io.sentry.** { *; }
+```
+
+The first rule prevents R8 from stripping btrace's HTTP server classes (fails with `SocketException` otherwise). The second preserves Sentry class and method names so they appear readable in the Perfetto trace instead of obfuscated single-letter names.
+
+## Step 3: Build and Install
+
+Prefer **debug builds** — they provide richer tracing instrumentation (Handler, MessageQueue, Monitor:Lock slices visible) which is essential for comparing internal SDK behavior. Use the default 1kHz btrace sampling rate for debug builds.
+
+```bash
+./gradlew :sentry-samples:sentry-samples-android:installDebug
+```
+
+**Release builds** are useful when you need to measure real-world performance without StrictMode/debuggable overhead or with R8 optimizations. Require the ProGuard keep rules from step 2d. Use `-sampleInterval 333000` (333μs / 3kHz) for finer granularity since release code runs faster.
+
+```bash
+./gradlew :sentry-samples:sentry-samples-android:installRelease
+```
+
+## Step 4: Capture Trace
+
+For each branch to trace:
+
+### 4a: Set btrace properties and launch app
+
+Clear any stale port files, set properties, and launch:
+
+```bash
+adb shell "rm -rf /storage/emulated/0/Android/data/io.sentry.samples.android/files/rhea-port"
+adb shell setprop debug.rhea3.startWhenAppLaunch 1
+adb shell setprop debug.rhea3.waitTraceTimeout 60
+adb shell am force-stop io.sentry.samples.android
+sleep 2
+adb shell am start -n io.sentry.samples.android/.MainActivity
+sleep 5
+```
+
+The app must be started AFTER `debug.rhea3.startWhenAppLaunch` is set, otherwise the trace server won't initialize. The 5s sleep after launch gives the btrace HTTP server time to start.
+
+### 4b: Play a sound to signal the user, then capture
+
+Play a sound when tracing actually starts so the user knows to begin interacting. Pipe btrace output through a loop that triggers the sound on the "start tracing" line:
+
+```bash
+java -jar tools/btrace/rhea-trace-shell.jar \
+ -a io.sentry.samples.android \
+ -t ${duration} \
+ -waitTraceTimeout 60 \
+ -o tools/btrace/traces/${branch_name}.pb \
+ sched 2>&1 | while IFS= read -r line; do
+ echo "$line"
+ if [[ "$line" == *"start tracing"* ]]; then
+ afplay -v 1.5 /System/Library/Sounds/Ping.aiff &
+ fi
+ done
+```
+
+For release builds with finer sampling, add `-sampleInterval 333000`.
+
+Do NOT use the `-r` flag — it fails to resolve the launcher activity because LeakCanary registers a second one. Launch the app manually in step 4a instead.
+
+### 4c: Switch branches for comparison
+
+When capturing a second branch:
+
+1. Stash the btrace integration changes:
+ ```bash
+ git stash push -m "btrace integration" -- \
+ sentry-samples/sentry-samples-android/build.gradle.kts \
+ sentry-samples/sentry-samples-android/src/main/java/io/sentry/samples/android/MyApplication.java \
+ sentry-samples/sentry-samples-android/proguard-rules.pro
+ ```
+2. Checkout the other branch
+3. Pop the stash: `git stash pop`
+4. Rebuild and install (same variant — debug or release — as the first branch)
+5. Repeat steps 4a and 4b with a different output filename
+6. Switch back to the original branch and restore files
+
+## Step 5: Open in Perfetto UI
+
+Generate a viewer HTML and serve it locally. Use the template at `assets/viewer-template.html` as a base — copy it to `tools/btrace/traces/viewer.html` and replace the placeholder values:
+
+- `TRACE_FILES`: array of `{file, title}` objects for each captured trace
+- `SQL_QUERY`: the SQL query to prefill
+
+The SQL query is passed via the URL hash parameter: `https://ui.perfetto.dev/#!/?query=...`
+
+The trace data is sent via the postMessage API (required for local files — URL deep-linking does not work with `file://`).
+
+Start a local HTTP server and open the viewer:
+
+```bash
+cd tools/btrace/traces && python3 -m http.server 8008 &
+open http://localhost:8008/viewer.html
+```
+
+### Default SQL Query
+
+If no custom query is provided, use:
+
+```sql
+SELECT
+ s.name AS slice_name,
+ s.dur / 1e6 AS dur_ms,
+ s.ts,
+ t.name AS track_name
+FROM slice s
+JOIN thread_track t ON s.track_id = t.id
+WHERE s.name GLOB '*SentryWindowCallback.dispatch*'
+ORDER BY s.ts
+```
+
+## Step 6: Query and Compare Traces
+
+After capturing both branches, use `trace_processor` to compute comparison stats locally.
+
+### Basic stats query
+
+For each trace file, run:
+
+```bash
+/tmp/trace_processor -Q "
+WITH events AS (
+ SELECT s.dur / 1e6 as dur_ms FROM slice s
+ WHERE s.name GLOB '*${METHOD_GLOB}*' AND s.dur > 0
+ ORDER BY s.dur
+)
+SELECT COUNT(*) as count,
+ ROUND(AVG(dur_ms), 4) as avg_ms,
+ ROUND((SELECT dur_ms FROM events LIMIT 1 OFFSET (SELECT COUNT(*)/2 FROM events)), 4) as median_ms,
+ ROUND(MIN(dur_ms), 4) as min_ms,
+ ROUND(MAX(dur_ms), 4) as max_ms
+FROM events
+" tools/btrace/traces/${trace_file}.pb
+```
+
+Replace `${METHOD_GLOB}` with the method pattern to compare (e.g. `SentryGestureDetector.onTouchEvent`, `SentryWindowCallback.dispatchTouchEvent`).
+
+### Finding child calls (debug builds)
+
+To find what happens inside a method (e.g. Handler calls, lock acquisitions):
+
+```bash
+/tmp/trace_processor -Q "
+WITH RECURSIVE descendants(id, depth) AS (
+ SELECT s.id, 0 FROM slice s WHERE s.name GLOB '*${PARENT_METHOD}*'
+ UNION ALL
+ SELECT s.id, d.depth + 1 FROM slice s JOIN descendants d ON s.parent_id = d.id WHERE d.depth < 10
+)
+SELECT s.name, COUNT(*) as count, ROUND(AVG(s.dur / 1e6), 3) as avg_ms
+FROM slice s JOIN descendants d ON s.id = d.id
+WHERE d.depth > 0
+GROUP BY s.name ORDER BY count DESC
+LIMIT 20
+" tools/btrace/traces/${trace_file}.pb
+```
+
+### Build the comparison table
+
+Run the stats query on both trace files, then present a markdown table:
+
+```
+| Metric | Branch A | Branch B | Delta |
+|--------|----------|----------|-------|
+| Count | ... | ... | |
+| Average| ... | ... | -X% |
+| Median | ... | ... | -X% |
+| Max | ... | ... | -X% |
+```
+
+Compute delta as `(branchA - branchB) / branchB * 100`. Negative means branch A is faster.
+
+### Sampling rate reference
+
+| Rate | Interval | `-sampleInterval` | Use case |
+|------|----------|-------------------|----------|
+| 1 kHz | 1ms | `1000000` (default) | Debug builds, general profiling |
+| 3 kHz | 333μs | `333000` | Release builds, finer granularity |
+| 10 kHz | 100μs | `100000` | Maximum detail, higher overhead |
+
+Higher sampling rates capture shorter method calls but add CPU overhead which can skew results. For most comparisons, the default 1kHz is sufficient.
+
+## Cleanup
+
+After tracing is complete, remind the user that the btrace integration changes to the sample app should NOT be committed. The `tools/btrace/` directory is gitignored.
+
+## Troubleshooting
+
+| Problem | Solution |
+|---------|----------|
+| `No compatible library found [shadowhook]` | Restrict `ndk.abiFilters` to arm64-v8a only |
+| `package com.bytedance.btrace does not exist` | Use `com.bytedance.rheatrace` (not `btrace`) |
+| `ResolverActivity does not exist` with `-r` flag | Don't use `-r`; launch the app manually before capturing |
+| `wait for trace ready timeout` on download | Set `debug.rhea3.startWhenAppLaunch=1` BEFORE launching the app, and use `-waitTraceTimeout 60` |
+| Empty jar file (0 bytes) | Download from Maven Central (`repo1.maven.org`), not `oss.sonatype.org` |
+| `FileNotFoundException` on sampling download | App was already running when properties were set; force-stop and relaunch |
+| `SocketException: Unexpected end of file` in release builds | R8 stripped btrace classes; add `-keep class com.bytedance.rheatrace.** { *; }` to proguard-rules.pro |
+| Stale port from previous session | Run `adb shell "rm -rf /storage/emulated/0/Android/data/io.sentry.samples.android/files/rhea-port"` before launching |
+| Most `onTouchEvent` durations are 0ms | Increase sampling rate with `-sampleInterval 333000` (3kHz) |
diff --git a/.claude/skills/btrace-perfetto/assets/viewer-template.html b/.claude/skills/btrace-perfetto/assets/viewer-template.html
new file mode 100644
index 00000000000..4c31a24c342
--- /dev/null
+++ b/.claude/skills/btrace-perfetto/assets/viewer-template.html
@@ -0,0 +1,49 @@
+
+
+
btrace Trace Viewer
+
+ Perfetto Trace Viewer
+
+
+
+
+
diff --git a/.claude/skills/check-code-attribution/SKILL.md b/.claude/skills/check-code-attribution/SKILL.md
new file mode 100644
index 00000000000..ee66327c260
--- /dev/null
+++ b/.claude/skills/check-code-attribution/SKILL.md
@@ -0,0 +1,244 @@
+---
+name: check-code-attribution
+description: Per-file check of vendored code attribution in the current branch diff, including license headers, THIRD_PARTY_NOTICES.md entries, and compatibility with Sentry's licensing policy
+allowed-tools: Bash Read Grep Glob
+---
+
+# Check Code Attribution
+
+You are reviewing changed files for third-party code attribution compliance in **sentry-java**, an MIT-licensed repository.
+
+## Local runs
+
+When running locally (not via Warden), review every file changed on this branch vs the base branch. Apply the same path exclusions as `ignorePaths` in `warden.toml`, then run Quick triage and the checks below on each file. For git commands to list changed files and Warden CLI setup, see `validation-tests/README.md`. `/check-code-attribution` in the IDE does not require Warden credentials.
+
+When running via Warden, the changed file is already provided — skip branch-wide discovery, but follow **Warden execution** below.
+
+## Warden execution
+
+Warden analyzes one changed file per run (whole-file mode). Complete every Quick triage step — the diff alone is not sufficient.
+
+**Mandatory on every run (do not skip):**
+
+1. Read the first 50 lines of the changed file.
+2. Search `THIRD_PARTY_NOTICES.md` for the class name (filename without extension, e.g. `ANRWatchDog` for `ANRWatchDog.java`). On renames, also search for the old basename and read Scope sections (see Quick triage).
+3. When you can compare against the base branch version, inspect the header at that revision (first 50 lines).
+
+**Do not dismiss findings because:**
+
+- A `THIRD_PARTY_NOTICES.md` entry exists — file headers are still required; NOTICES does not replace them.
+- The diff only removes a header comment block — if removed `-` lines include a **required field** (see below) or vendoring language ("adapted from", etc.), attribution was stripped. Removing boilerplate alone is not stripping.
+- The header says "Adapted from …" but omits copyright holder or license name — flag missing header fields.
+- The file header has all four required fields — a missing THIRD_PARTY_NOTICES.md entry is independently required and is ⚠️ medium regardless of header completeness.
+
+For `THIRD_PARTY_NOTICES.md` runs: for every **removed** entry in the diff, confirm whether Scope files still exist with attribution headers. If they do, the entry must not be removed.
+
+## Quick triage
+
+Sentry's own files carry **no** copyright headers — any copyright/license line indicates third-party code. Every file that reaches this skill is in scope — do not skip files based on extension.
+
+If this file is `THIRD_PARTY_NOTICES.md`, go to the THIRD_PARTY_NOTICES section below.
+
+For all other files, perform these checks **before** deciding whether to proceed:
+
+1. **Read the file header** — inspect the first 50 lines. Look for vendored-code signals: `Copyright`, `Licensed under`, `SPDX-License-Identifier`, or vendoring language ("adapted from", "backported from", "based on", "copied from", "derived from", "inspired by", "ported from", "translated from", "vendored").
+2. **Check THIRD_PARTY_NOTICES.md** — search for the file name without extension (e.g. `ANRWatchDog` when reviewing `ANRWatchDog.java`). A match means this is a known vendored file. **Renames:** if the diff is a rename (`similarity index` / `rename from` in the diff, or a delete of one path and add of another with the same content), also search for the **old** basename and read **Scope** sections in matching entries — NOTICES may still reference the previous class or path name.
+ > **A complete NOTICES entry does NOT end the check.** It confirms the file is vendored and that the NOTICES requirement is satisfied. The file header is a separate, additional requirement — continue to header verification regardless of NOTICES completeness.
+3. **Scan the diff** — check for vendored-code signals on both added (`+`) and **removed (`-`)** lines. Removed lines that drop a **required field** (copyright, license name, source URL, vendoring origin) ARE signals. Removed disclaimer/boilerplate lines alone are not.
+
+**A signal in ANY of these three sources means this is vendored code — proceed to the vendored source file section.**
+
+A file referenced in THIRD_PARTY_NOTICES.md is ALWAYS vendored, even if its current header has no attribution.
+
+**If none of the three sources have signals, report no findings and stop.**
+
+---
+
+## If this file is `THIRD_PARTY_NOTICES.md`
+
+Validate the changed entries using the diff context:
+
+1. For each added or modified entry, verify it has all required fields: **Source URL**, **License name**, **Copyright**, **Scope** (file paths), and **full license text** in a fenced code block.
+2. For each Scope path, verify the file(s) exist.
+3. Flag new license types using the same license-tier table as for source files: weak copyleft (LGPL, MPL, EPL) → 🚨 **high**, strong copyleft (GPL) → 🚨 **high**, AGPL → 🚨 **high** (absolute ban, must be removed). Do not use low or medium for copyleft or AGPL.
+4. Flag orphaned entries whose Scope files no longer exist.
+5. For **removed** entries (lines prefixed with `-` in the diff), check whether the Scope files still exist and still have attribution headers. If they do, the entry must not be removed.
+6. Check **copyright consistency** — the Copyright field must match the copyright line inside the embedded license text. Flag mismatches.
+
+---
+
+## If this is a vendored file
+
+### 1. Check attribution header
+
+Check each of the following by reading the file header — not NOTICES. Each is an independent yes/no; a "no" is ⚠️ medium regardless of NOTICES completeness:
+
+- [ ] **Vendoring origin phrase** — explicit wording such as `Adapted from …`, `Based on …`, `Vendored from …`, or a library name.
+- [ ] **Copyright line** — e.g. `Copyright (c) 2016 …`, `Copyright 2010 Square, Inc.`
+- [ ] **License name** — e.g. `Licensed under the Apache License, Version 2.0`, `The MIT License`
+- [ ] **Source URL** — e.g. `https://github.com/…`
+
+Exact wording and comment style may vary. **Do not flag** missing or changed content that is not one of these four fields.
+
+**Each field must be physically present in the file header. A complete `THIRD_PARTY_NOTICES.md` entry does not satisfy any required field — both are independently required. Check each of the four fields by reading the file header, not by reasoning from NOTICES.**
+
+**Not required in the file header** (full text belongs in `THIRD_PARTY_NOTICES.md`, not in every source file):
+
+- Full license boilerplate (MIT permission paragraph, Apache "Unless required by applicable law…" disclaimer, ASF contributor grant preamble)
+- Wording differences vs the NOTICES embedded license text (e.g. shortened Apache header vs canonical ASF phrasing)
+- Comment style (`//` vs `/* */`), line wrapping, or extra Sentry modification notes
+
+Compare the current header against the NOTICES entry **only for the four required fields** — e.g. if NOTICES says MIT by "Salomon BRYS" but the header has no copyright or license name, flag it. If both have copyright + license name but the header omits the Apache disclaimer while NOTICES still has the full text, **do not flag**.
+
+When comparing against the base branch version (local runs), use the header at that revision for additional context.
+
+Flag these issues:
+- **Header stripped** — file is in NOTICES but current header has none of the four required fields
+- **Header truncated** — one or more **required** fields were removed (e.g. copyright line or `Licensed under …` removed) while the file remains vendored
+- **Header inconsistent** — a **required** field contradicts NOTICES (wrong copyright holder/year, wrong license name) — not boilerplate or phrasing differences
+- **Diff removes required attribution** — removed `-` lines drop a required field or vendoring origin (`Adapted from`, etc.); removing disclaimer/boilerplate lines alone is **not** this
+
+**Do not report** (no finding — prefer silence):
+
+- Apache/MIT disclaimer or permission paragraphs removed but all four required fields remain
+- Header reworded to a shorter permissive-license form with the same copyright holder and license name
+- Header and NOTICES differ only in full license body text (wording or boilerplate, not missing required fields)
+
+These exceptions apply only when an entry already exists in NOTICES and only to header-vs-NOTICES wording differences. A **missing** NOTICES entry is ⚠️ medium per section 2 — never covered by these exceptions.
+
+### 2. Check THIRD_PARTY_NOTICES.md entry
+
+**Severity: always `medium`. Do not output `severity: "low"` for a missing entry even if the attribution header is complete.**
+
+`THIRD_PARTY_NOTICES.md` is a mandatory legal exhibit that Sentry ships with every SDK distribution. It must enumerate all vendored code regardless of what the source file header says. A missing entry is a distribution-level compliance failure, not a nit. A complete file header does not satisfy the NOTICES requirement — both are mandatory.
+
+From the NOTICES search in Quick triage: if no matching entry exists, output `severity: "medium"` and flag as ⚠️ Missing THIRD_PARTY_NOTICES.md entry. A valid entry needs: Source URL, License name, Copyright, Scope, full license text.
+
+### 3. Check license compatibility
+
+Classify the license per Sentry's Open Source Legal Policy (https://open.sentry.io/licensing/):
+
+| Tier | Examples | Finding |
+|-----------------|-------------------------------------------------|---------------------------------------------|
+| Permissive | MIT, BSD, Apache 2.0, ISC, CC0, Unlicense, Zlib | None — license is compatible |
+| Weak copyleft | LGPL, MPL, EPL, CDDL | 🚨 **high** — requires review |
+| Strong copyleft | GPL, QPL, Sleepycat, OSL | 🚨 **high** — requires legal review |
+| AGPL | — | 🚨 **high** — absolute ban, must be removed |
+| No license | — | 🚨 **high** — assume no permission |
+
+**Permissive licenses:** do not report a finding solely because the license is MIT/BSD/Apache/etc. Only flag missing or stripped **required** header fields, or missing/inconsistent `THIRD_PARTY_NOTICES.md` entry. Do not flag disclaimer/boilerplate-only diffs. Copyleft and unlicensed code still get 🚨 findings per the table.
+
+---
+
+## If this is a deleted vendored file
+
+If the diff deletes a file and the removed lines contained attribution headers, check whether `THIRD_PARTY_NOTICES.md` still references it — the entry should be updated or removed.
+
+---
+
+## Severity guide
+
+| Level | Use for |
+|------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| **high** | 🚨 License violations: AGPL, copyleft, unlicensed, no-license code |
+| **medium** | ⚠️ Missing **required** header fields, stripped required fields, missing/inconsistent NOTICES entries (even when header is complete), deleted/renamed vendored files needing NOTICES update |
+| **low** | 👀 Cosmetic/style differences only (shortened license wording, comment style). **Never** use for a missing NOTICES entry or missing header field — those are always medium. |
+
+Warden relies on these severity levels when deciding whether to comment on PRs or require changes. Put the severity emoji **only on the finding title** (see Output) so reviewers can triage at a glance.
+
+## Output
+
+**No issues → empty response (say nothing).**
+
+Otherwise, report each finding ordered by severity (most severe first).
+
+### Emoji placement (required)
+
+Use the emoji from the severity guide (🚨, ⚠️, or 👀) — not the word `high`, `medium`, or `low`.
+
+| Field | Emoji? | Example |
+|-------------------|--------------------------|----------------------------------------------------------------------------------------------------------------------------------------|
+| **Title** | Yes — once, at the start | `⚠️ Copyright line stripped from vendored file header` |
+| **Description** | **No** | `**io.sentry.cache.tape.FileObjectQueue** — The Copyright (C) 2010 Square, Inc. line was removed…` (see **Description subject** below) |
+| **Verification** | **No** | Evidence steps only |
+| **Suggested fix** | **No** | Fix text only |
+
+**Good (Warden PR comment):**
+
+```
+Title: ⚠️ Copyright line stripped from vendored file header
+Description: **io.sentry.cache.tape.FileObjectQueue** — The `Copyright (C) 2010 Square, Inc.` line was removed from this vendored file's header. Please restore the copyright line.
+```
+
+**Bad — emoji in the description (never do this):**
+
+```
+Title: ⚠️ Copyright line stripped from vendored file header
+Description: ⚠️ The `Copyright (C) 2010 Square, Inc.` line was removed…
+```
+
+**Bad — emoji before the class name:**
+
+```
+Title: ⚠️ Copyright line stripped from vendored file header
+Description: ⚠️ **io.sentry.cache.tape.FileObjectQueue** — The copyright line was removed…
+```
+
+### Description subject (required)
+
+Every description **must** start with `**** —` (bold subject, space, em dash, space). Pick **one** subject by file type:
+
+| File type | Subject format | Example |
+|-------------------------------------------------------------------------------------------|----------------------------------------------------------------------|----------------------------------------------------------------|
+| Java / Kotlin source (`.java`, `.kt`) with a top-level type | Fully qualified class name (FQCN) | `**io.sentry.CircularFifoQueue** —` |
+| Java / Kotlin with no single clear type (multiple top-level types, unclear which changed) | FQCN of the primary type under review, or repo-relative path if none | `**sentry/src/.../Foo.kt** —` |
+| `THIRD_PARTY_NOTICES.md` | `THIRD_PARTY_NOTICES.md — ` | `**THIRD_PARTY_NOTICES.md — Square — Seismic (Apache 2.0)** —` |
+| Gradle / other scripts (e.g. `.kts`, `.gradle`) | Repo-relative path from repository root | `**build.gradle.kts** —` |
+
+- Prefer **FQCN** for `.java` / `.kt` vendored source (derive from `package` + primary public top-level class). Do not use file paths when a FQCN is clear.
+- For license-tier / policy issues, include https://open.sentry.io/licensing/ in the description body.
+
+### Warden runs
+
+For each finding, set these fields exactly:
+
+| Field | Value |
+|------------------|-------------------------------------------------------------------------------------------------------------------|
+| **severity** | `high`, `medium`, or `low` — **never** put emoji here; Warden maps severity from this field, not from the title |
+| **title** | ` ` — emoji allowed **only** here (imperative, no class name) |
+| **description** | `**** — ` — **plain text only**; subject per **Description subject** above |
+| **verification** | Optional evidence steps — plain text only |
+
+**Description rules (Warden):**
+
+- **Must** match `**** — …` using the table in **Description subject**.
+- **Must not** contain 🚨, ⚠️, 👀, or the words `high`, `medium`, or `low` as severity labels.
+- **Must not** repeat the title or paraphrase it with an emoji prefix.
+
+**Good (NOTICES entry removed while scope files remain):**
+
+```
+Title: ⚠️ NOTICES entry removed for vendored code still in tree
+Description: **THIRD_PARTY_NOTICES.md — Square — Seismic (Apache 2.0)** — The Seismic entry was removed but `io.sentry.android.core.SentryShakeDetector` still has an attribution header. Restore the entry or remove attribution from the scope files.
+```
+
+**Before submitting findings:** For every finding, confirm `description` does not match `[🚨⚠️👀]` and matches `^\*\*.+\*\* — `. If it contains any emoji, rewrite the description without it.
+
+### Local / IDE runs
+
+Use this numbered format — same title vs description split as above:
+
+```
+1\. ****
+ **** —
+
+2\. ****
+ **** —
+```
+
+Rules:
+
+- Put the severity emoji **only** on the title line (`1\. ⚠️ **…**`), never on the description line.
+- The description line uses `**** —` per **Description subject** and must not contain 🚨, ⚠️, or 👀.
+- **Escape the period** after the number (`1\.` not `1.`) so markdown does not collapse entries into a tight list.
+- Leave an empty line between each numbered finding.
diff --git a/.claude/skills/check-code-attribution/validation-tests/EXPECTED.json b/.claude/skills/check-code-attribution/validation-tests/EXPECTED.json
new file mode 100644
index 00000000000..a82637b84e2
--- /dev/null
+++ b/.claude/skills/check-code-attribution/validation-tests/EXPECTED.json
@@ -0,0 +1,53 @@
+[
+ {
+ "id": "header-complete-and-notice-present",
+ "file": "HeaderCompleteAndNoticePresent.java",
+ "expectFinding": false,
+ "notes": "Header matches catalog entry"
+ },
+ {
+ "id": "header-complete-but-notice-missing",
+ "file": "HeaderCompleteButNoticeMissing.java",
+ "expectFinding": true,
+ "isolated": true,
+ "notes": "Full header; no catalog / root NOTICES entry. Isolated: prompt-cache priming in a concurrent batch suppresses the missing-NOTICES finding below medium."
+ },
+ {
+ "id": "header-missing-but-notice-present",
+ "file": "HeaderMissingButNoticePresent.java",
+ "expectFinding": true,
+ "isolated": true,
+ "notes": "NOTICES entry claims file is vendored but file has no attribution header. Isolated: a complete NOTICES entry suppresses the missing-header finding in a concurrent batch."
+ },
+ {
+ "id": "header-fully-stripped",
+ "file": "HeaderFullyStripped.java",
+ "expectFinding": true,
+ "notes": "Header has no required attribution fields"
+ },
+ {
+ "id": "header-partially-stripped",
+ "file": "HeaderPartiallyStripped.java",
+ "expectFinding": true,
+ "notes": "Adapted from + URL only; no copyright or license name"
+ },
+ {
+ "id": "header-missing-non-essential-info",
+ "file": "HeaderMissingNonEssentialInfo.java",
+ "expectFinding": false,
+ "notes": "All four required fields present; no license boilerplate — boilerplate is not required in the header"
+ },
+ {
+ "id": "header-vs-notice-mismatch",
+ "file": "THIRD_PARTY_NOTICES.md",
+ "expectFinding": true,
+ "isolated": true,
+ "notes": "Copyright in metadata field does not match embedded license text. Isolated: mismatch finding needs an independent assertion free of interference from other NOTICES changes."
+ },
+ {
+ "id": "new-license-type",
+ "file": "NewLicenseType.java",
+ "expectFinding": true,
+ "notes": "AGPL v3 license in file header — absolute ban, must be removed"
+ }
+]
diff --git a/.claude/skills/check-code-attribution/validation-tests/README.md b/.claude/skills/check-code-attribution/validation-tests/README.md
new file mode 100644
index 00000000000..99fb42a6836
--- /dev/null
+++ b/.claude/skills/check-code-attribution/validation-tests/README.md
@@ -0,0 +1,86 @@
+# Attribution skill validation tests
+
+Self-contained samples for validating `check-code-attribution` without touching production SDK sources.
+
+
+## Run the tests
+
+```bash
+./check-code-attribution-tests.sh
+```
+
+Requires Node.js and a Warden provider (see **Warden CLI** below).
+
+In practice, straight command line runs tend to be a bit flakier than asking Claude Code to run the tests for you.
+
+## Local development
+
+### Discovering changed files
+
+When running `/check-code-attribution` outside Warden, list files changed on the current branch vs the base branch, then apply the same exclusions as `ignorePaths` in `warden.toml`:
+
+```bash
+MB=$(git merge-base HEAD origin/main 2>/dev/null || git merge-base HEAD main)
+git diff --name-only "${MB}"..HEAD
+```
+
+### Warden CLI
+
+Warden does **not** use Cursor auth. Before running Warden locally, configure a provider (same model family as `warden.toml`, or override with `-m`):
+
+```bash
+# Option A: Anthropic API key (matches CI model in warden.toml)
+export WARDEN_ANTHROPIC_API_KEY=sk-ant-... # or: export ANTHROPIC_API_KEY=sk-ant-...
+
+# Option B: Pi OAuth / API key store (~/.pi/agent/auth.json)
+npx pi # then run /login and pick Anthropic (or another provider)
+
+# Option C: Different provider for a one-off run
+export WARDEN_OPENAI_API_KEY=sk-...
+npx @sentry/warden origin/main..HEAD --skill check-code-attribution -m openai/gpt-5.5 -vv
+```
+
+```bash
+npx @sentry/warden origin/main..HEAD --skill check-code-attribution -vv
+```
+
+## Layout
+
+- `EXPECTED.json` — scenario IDs and expected outcomes (single source of truth).
+- `THIRD_PARTY_NOTICES.catalog.md` — NOTICES-style entries for validation class names.
+- `scenarios/` — `.java` files and `THIRD_PARTY_NOTICES.mismatch-snippet.md` (copyright-mismatch fixture).
+- `check-code-attribution-tests.sh` — runs Warden on a temp branch and asserts per-scenario pass/fail.
+- `assert-scenarios.mjs` — validation driver (`list-isolated`, `routing-set`, `assert` subcommands); parses Warden JSONL and checks outcomes from `EXPECTED.json`.
+
+### assert-scenarios.mjs commands
+
+```bash
+node assert-scenarios.mjs validate EXPECTED.json scenarios/ # pre-flight (no API); run automatically by the shell script
+node assert-scenarios.mjs list-isolated EXPECTED.json # idfile per isolated scenario
+node assert-scenarios.mjs list-main-java EXPECTED.json scenarios/ # .java files for the main Warden batch
+node assert-scenarios.mjs routing-set routing.json # update id → Warden JSONL path
+node assert-scenarios.mjs assert EXPECTED.json routing.json
+```
+
+Warden runs are limited to 300s. On macOS the script uses `gtimeout` (from `brew install coreutils`) when available, otherwise GNU `timeout`, otherwise `perl` with `alarm`.
+
+## Add a scenario
+
+1. Add `scenarios/.java`.
+2. Add or omit a catalog entry in `THIRD_PARTY_NOTICES.catalog.md`.
+3. Add an entry to `EXPECTED.json`.
+4. **Isolation (if needed):** If the scenario relies on a finding that could be suppressed by Anthropic prompt-cache priming when analyzed alongside many other files (e.g. a missing-NOTICES entry, or a missing header on a file that has a complete NOTICES entry), add `"isolated": true` to its `EXPECTED.json` entry. The test script creates a dedicated worktree for each isolated scenario automatically — no changes to the script itself are needed.
+
+## Validation (maintainers)
+
+Test samples live under `validation-tests/` and are excluded from normal skill runs via `.claude/**` in `warden.toml`.
+
+```bash
+.claude/skills/check-code-attribution/validation-tests/check-code-attribution-tests.sh
+```
+
+Expected outcomes are in `EXPECTED.json`. The script creates isolated git worktrees, runs Warden with `--report-on medium --json`, and asserts per-scenario pass/fail. Scenarios marked `"isolated": true` in `EXPECTED.json` each get their own worktree to avoid Anthropic prompt-cache priming that can suppress findings below medium in concurrent batches. Exit 0 = all pass.
+
+When manually reviewing a file under `scenarios/`, search `THIRD_PARTY_NOTICES.catalog.md` in addition to root `THIRD_PARTY_NOTICES.md` (Quick triage step 2 in `SKILL.md`).
+
+Non-Java fixtures required by the test script are listed in `REQUIRED_SCENARIO_FIXTURES` in `assert-scenarios.mjs`; pre-flight `validate` fails if any are missing.
diff --git a/.claude/skills/check-code-attribution/validation-tests/THIRD_PARTY_NOTICES.catalog.md b/.claude/skills/check-code-attribution/validation-tests/THIRD_PARTY_NOTICES.catalog.md
new file mode 100644
index 00000000000..478d0b06313
--- /dev/null
+++ b/.claude/skills/check-code-attribution/validation-tests/THIRD_PARTY_NOTICES.catalog.md
@@ -0,0 +1,130 @@
+# Test THIRD_PARTY_NOTICES catalog (not shipped)
+
+Used only when validating `check-code-attribution` against `validation-tests/scenarios/**`.
+Grep this file in addition to the repository root `THIRD_PARTY_NOTICES.md`.
+
+---
+
+## Example — HeaderFullyStripped (MIT)
+
+**Source:** https://github.com/example/attribution-fixtures
+**License:** MIT License
+**Copyright:** Copyright (c) 2016 Example Author
+
+### Scope
+
+Attribution validation sample. The code resides in `io.sentry.skills.verification.HeaderFullyStripped` (`validation-tests/scenarios/HeaderFullyStripped.java`).
+
+```
+MIT License
+
+Copyright (c) 2016 Example Author
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+```
+
+---
+
+## Example — HeaderMissingButNoticePresent (Apache 2.0)
+
+**Source:** https://github.com/example/notices-without-header
+**License:** Apache License 2.0
+**Copyright:** Copyright 2023 Example Corp.
+
+### Scope
+
+Attribution validation sample. The code resides in `io.sentry.skills.verification.HeaderMissingButNoticePresent`.
+
+```
+Copyright 2023 Example Corp.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+```
+
+---
+
+## Example — HeaderMissingNonEssentialInfo (MIT)
+
+**Source:** https://github.com/example/examplelib
+**License:** MIT License
+**Copyright:** Copyright 2020 Example Corp.
+
+### Scope
+
+Attribution validation sample. The code resides in `io.sentry.skills.verification.HeaderMissingNonEssentialInfo`.
+
+```
+MIT License
+
+Copyright (c) 2020 Example Corp.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+```
+
+---
+
+## Example — HeaderCompleteAndNoticePresent (Apache 2.0)
+
+**Source:** https://github.com/example/something
+**License:** Apache License 2.0
+**Copyright:** Copyright 2020 Example Authors
+
+### Scope
+
+Attribution validation sample. The code resides in `io.sentry.skills.verification.HeaderCompleteAndNoticePresent`.
+
+```
+Copyright 2020 Example Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+```
diff --git a/.claude/skills/check-code-attribution/validation-tests/assert-scenarios.mjs b/.claude/skills/check-code-attribution/validation-tests/assert-scenarios.mjs
new file mode 100755
index 00000000000..3ff4cce9980
--- /dev/null
+++ b/.claude/skills/check-code-attribution/validation-tests/assert-scenarios.mjs
@@ -0,0 +1,401 @@
+#!/usr/bin/env node
+/**
+ * Validation driver for check-code-attribution scenario tests.
+ *
+ * Usage:
+ * node assert-scenarios.mjs validate
+ * node assert-scenarios.mjs list-isolated
+ * node assert-scenarios.mjs list-main-java
+ * node assert-scenarios.mjs routing-set
+ * node assert-scenarios.mjs assert
+ *
+ * routing.json maps scenario id to Warden JSONL output path, e.g. { "main": "/tmp/..." }.
+ * Non-isolated scenarios use the "main" entry when no dedicated id is present.
+ */
+
+import fs from 'node:fs';
+import path from 'node:path';
+import { pathToFileURL } from 'node:url';
+
+const ISOLATED_FILE_JAVA = /\.java$/i;
+const ISOLATED_FILE_NOTICES = 'THIRD_PARTY_NOTICES.md';
+
+/** Non-Java fixtures under scenarios/ that check-code-attribution-tests.sh requires. */
+const REQUIRED_SCENARIO_FIXTURES = [
+ 'THIRD_PARTY_NOTICES.mismatch-snippet.md',
+];
+
+export function loadExpected(expectedPath) {
+ return JSON.parse(fs.readFileSync(expectedPath, 'utf8'));
+}
+
+export function listIsolated(scenarios) {
+ return scenarios.filter((s) => s.isolated);
+}
+
+/** Repo-relative path normalization for Warden JSONL matching. */
+export function normalizeRepoPath(filePath) {
+ if (!filePath) return filePath;
+ return filePath.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+/g, '/');
+}
+
+/** True when a Warden-reported path refers to the expected scenario file. */
+export function pathMatchesWardenFile(reportedPath, wardenFile) {
+ const reported = normalizeRepoPath(reportedPath);
+ const expected = normalizeRepoPath(wardenFile);
+ if (reported === expected) return true;
+ const base = expected.split('/').pop();
+ return base != null && reported.endsWith(`/${base}`);
+}
+
+export function findingCountForFile(fileMap, wardenFile) {
+ const expected = normalizeRepoPath(wardenFile);
+ if (fileMap[expected] != null) return fileMap[expected];
+ for (const [key, count] of Object.entries(fileMap)) {
+ if (pathMatchesWardenFile(key, wardenFile)) return count;
+ }
+ return 0;
+}
+
+export function findingsForFile(findings, wardenFile) {
+ return findings.filter(
+ (f) => f.location && pathMatchesWardenFile(f.location.path, wardenFile),
+ );
+}
+
+export function listMainBatchJava(scenarios, scenariosDir) {
+ const isolatedJava = new Set(
+ listIsolated(scenarios)
+ .map((s) => s.file)
+ .filter((file) => ISOLATED_FILE_JAVA.test(file)),
+ );
+ return fs
+ .readdirSync(scenariosDir)
+ .filter((name) => name.endsWith('.java') && !isolatedJava.has(name))
+ .sort();
+}
+
+/**
+ * @returns {string[]} validation error messages (empty = ok)
+ */
+export function validateExpected(scenarios, scenariosDir) {
+ const errors = [];
+
+ if (!Array.isArray(scenarios)) {
+ return ['EXPECTED.json must be a JSON array'];
+ }
+
+ const ids = new Set();
+ const expectedJava = new Set();
+
+ for (const [index, s] of scenarios.entries()) {
+ const label = `entry ${index}`;
+ if (!s || typeof s !== 'object') {
+ errors.push(`${label}: must be an object`);
+ continue;
+ }
+ if (typeof s.id !== 'string' || !s.id) {
+ errors.push(`${label}: missing or empty "id"`);
+ } else {
+ if (ids.has(s.id)) errors.push(`duplicate id "${s.id}"`);
+ ids.add(s.id);
+ if (s.id === 'main') {
+ errors.push(`id "main" is reserved for routing.json`);
+ }
+ }
+ if (typeof s.file !== 'string' || !s.file) {
+ errors.push(`${label}: missing or empty "file"`);
+ } else if (ISOLATED_FILE_JAVA.test(s.file)) {
+ expectedJava.add(s.file);
+ const onDisk = path.join(scenariosDir, s.file);
+ if (!fs.existsSync(onDisk)) {
+ errors.push(`${s.id}: scenarios/${s.file} does not exist`);
+ }
+ } else if (s.file !== ISOLATED_FILE_NOTICES) {
+ errors.push(
+ `${s.id}: unsupported file "${s.file}" (use *.java or ${ISOLATED_FILE_NOTICES})`,
+ );
+ }
+ if (typeof s.expectFinding !== 'boolean') {
+ errors.push(`${s.id ?? label}: "expectFinding" must be a boolean`);
+ }
+ if (s.isolated) {
+ if (
+ !ISOLATED_FILE_JAVA.test(s.file) &&
+ s.file !== ISOLATED_FILE_NOTICES
+ ) {
+ errors.push(
+ `${s.id}: isolated scenarios must use *.java or ${ISOLATED_FILE_NOTICES}`,
+ );
+ }
+ }
+ }
+
+ let diskEntries = [];
+ try {
+ diskEntries = fs.readdirSync(scenariosDir);
+ } catch (e) {
+ errors.push(`cannot read scenarios dir ${scenariosDir}: ${e.message}`);
+ return errors;
+ }
+
+ const diskJava = diskEntries.filter((n) => n.endsWith('.java'));
+ for (const name of diskJava) {
+ if (!expectedJava.has(name)) {
+ errors.push(`scenarios/${name} has no matching entry in EXPECTED.json`);
+ }
+ }
+
+ for (const name of REQUIRED_SCENARIO_FIXTURES) {
+ const onDisk = path.join(scenariosDir, name);
+ if (!fs.existsSync(onDisk)) {
+ errors.push(`scenarios/${name} is required but missing`);
+ }
+ }
+
+ const diskNonJava = diskEntries.filter(
+ (n) => !n.endsWith('.java') && fs.statSync(path.join(scenariosDir, n)).isFile(),
+ );
+ for (const name of diskNonJava) {
+ if (!REQUIRED_SCENARIO_FIXTURES.includes(name)) {
+ errors.push(
+ `scenarios/${name} is not listed in REQUIRED_SCENARIO_FIXTURES (update assert-scenarios.mjs)`,
+ );
+ }
+ }
+
+ if (listMainBatchJava(scenarios, scenariosDir).length === 0) {
+ errors.push('main Warden batch needs at least one non-isolated .java scenario');
+ }
+
+ return errors;
+}
+
+export function parseWardenJsonl(jsonlPath) {
+ /** @type {Record} */
+ const fileMap = {};
+ const allFindings = [];
+ try {
+ const raw = fs.readFileSync(jsonlPath, 'utf8').trim();
+ if (!raw) return { fileMap, findings: [] };
+ const records = raw
+ .split('\n')
+ .filter((l) => l.trim())
+ .map((l) => JSON.parse(l));
+ for (const record of records) {
+ const file = record.chunk && record.chunk.file;
+ if (!file) continue;
+ const normalized = normalizeRepoPath(file);
+ const recordFindings = record.findings || [];
+ fileMap[normalized] = (fileMap[normalized] || 0) + recordFindings.length;
+ for (const f of recordFindings) {
+ allFindings.push({
+ ...f,
+ location: f.location || { path: normalized, startLine: 1 },
+ });
+ }
+ }
+ } catch (e) {
+ console.error(
+ 'ERROR: Could not parse Warden output from ' + jsonlPath + ':',
+ e.message,
+ );
+ process.exit(2);
+ }
+ return { fileMap, findings: allFindings };
+}
+
+export function routingSet(routingPath, id, jsonlPath) {
+ const routing = JSON.parse(fs.readFileSync(routingPath, 'utf8'));
+ routing[id] = jsonlPath;
+ fs.writeFileSync(routingPath, JSON.stringify(routing));
+}
+
+function wardenFileForScenario(destPkg, scenario) {
+ return scenario.file === ISOLATED_FILE_NOTICES
+ ? ISOLATED_FILE_NOTICES
+ : `${destPkg}/${scenario.file}`;
+}
+
+function loadRouting(routingPath) {
+ /** @type {Record} */
+ let routing;
+ try {
+ routing = JSON.parse(fs.readFileSync(routingPath, 'utf8'));
+ } catch (e) {
+ console.error(`ERROR: Could not read routing file ${routingPath}:`, e.message);
+ process.exit(2);
+ }
+
+ if (typeof routing.main !== 'string' || !routing.main) {
+ console.error('ERROR: routing.json must include a non-empty "main" JSONL path.');
+ process.exit(2);
+ }
+ return routing;
+}
+
+function cmdValidate(expectedPath, scenariosDir) {
+ if (!expectedPath || !scenariosDir) {
+ console.error(
+ 'Usage: node assert-scenarios.mjs validate ',
+ );
+ process.exit(2);
+ }
+ const errors = validateExpected(loadExpected(expectedPath), scenariosDir);
+ if (errors.length > 0) {
+ console.error('EXPECTED.json validation failed:');
+ for (const err of errors) console.error(` - ${err}`);
+ process.exit(1);
+ }
+ console.log('EXPECTED.json OK');
+}
+
+function cmdListIsolated(expectedPath) {
+ for (const s of listIsolated(loadExpected(expectedPath))) {
+ process.stdout.write(`${s.id}\t${s.file}\n`);
+ }
+}
+
+function cmdListMainJava(expectedPath, scenariosDir) {
+ if (!expectedPath || !scenariosDir) {
+ console.error(
+ 'Usage: node assert-scenarios.mjs list-main-java ',
+ );
+ process.exit(2);
+ }
+ for (const name of listMainBatchJava(loadExpected(expectedPath), scenariosDir)) {
+ process.stdout.write(`${name}\n`);
+ }
+}
+
+function cmdRoutingSet(routingPath, id, jsonlPath) {
+ if (!routingPath || !id || !jsonlPath) {
+ console.error(
+ 'Usage: node assert-scenarios.mjs routing-set ',
+ );
+ process.exit(2);
+ }
+ routingSet(routingPath, id, jsonlPath);
+}
+
+function cmdAssert(expectedPath, destPkg, routingPath) {
+ if (!expectedPath || !destPkg || !routingPath) {
+ console.error(
+ 'Usage: node assert-scenarios.mjs assert ',
+ );
+ process.exit(2);
+ }
+
+ const routing = loadRouting(routingPath);
+ const scenarios = loadExpected(expectedPath);
+
+ /** @type {Record>} */
+ const parsed = {};
+ function getSource(id) {
+ const jsonlPath = routing[id] ?? routing.main;
+ if (!parsed[jsonlPath]) parsed[jsonlPath] = parseWardenJsonl(jsonlPath);
+ return parsed[jsonlPath];
+ }
+
+ const GREEN = '\x1b[32m';
+ const RED = '\x1b[31m';
+ const RESET = '\x1b[0m';
+
+ const failures = [];
+ let pass = 0;
+
+ for (const s of scenarios) {
+ if (s.isolated && !routing[s.id]) {
+ console.error(
+ `ERROR: isolated scenario "${s.id}" has no routing entry (missing Warden run?)`,
+ );
+ process.exit(2);
+ }
+
+ const wardenFile = wardenFileForScenario(destPkg, s);
+ const source = getSource(s.id);
+ const count = findingCountForFile(source.fileMap, wardenFile);
+ const passed = s.expectFinding ? count > 0 : count === 0;
+
+ if (passed) {
+ console.log(`${GREEN}PASS${RESET} ${s.id}`);
+ pass++;
+ } else {
+ const reason = s.expectFinding
+ ? 'expected finding (>= medium), got none'
+ : `expected no finding (>= medium), got ${count}`;
+ console.log(`${RED}FAIL${RESET} ${s.id} (${reason})`);
+
+ failures.push({
+ id: s.id,
+ findings: findingsForFile(source.findings, wardenFile),
+ });
+ }
+ }
+
+ const total = scenarios.length;
+ console.log('');
+ console.log(`${total} scenarios: ${pass} passed, ${total - pass} failed`);
+
+ if (failures.length > 0) {
+ console.log('');
+ console.log('Warden output');
+ console.log('══════════════════════');
+
+ for (const { id, findings } of failures) {
+ console.log('');
+ console.log(id);
+ console.log('-'.repeat(id.length));
+ if (findings.length === 0) {
+ console.log('(Warden produced no findings for this file)');
+ } else {
+ for (const f of findings) {
+ console.log(f.title);
+ if (f.description) console.log(f.description);
+ if (f.verification) console.log('\nVerification: ' + f.verification);
+ console.log('');
+ }
+ }
+ }
+
+ process.exit(1);
+ }
+}
+
+function usage() {
+ console.error(`Usage:
+ node assert-scenarios.mjs validate
+ node assert-scenarios.mjs list-isolated
+ node assert-scenarios.mjs list-main-java
+ node assert-scenarios.mjs routing-set
+ node assert-scenarios.mjs assert `);
+ process.exit(2);
+}
+
+function main() {
+ const [, , cmd, ...args] = process.argv;
+ switch (cmd) {
+ case 'validate':
+ cmdValidate(args[0], args[1]);
+ break;
+ case 'list-isolated':
+ if (!args[0]) usage();
+ cmdListIsolated(args[0]);
+ break;
+ case 'list-main-java':
+ cmdListMainJava(args[0], args[1]);
+ break;
+ case 'routing-set':
+ cmdRoutingSet(args[0], args[1], args[2]);
+ break;
+ case 'assert':
+ cmdAssert(args[0], args[1], args[2]);
+ break;
+ default:
+ usage();
+ }
+}
+
+if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
+ main();
+}
diff --git a/.claude/skills/check-code-attribution/validation-tests/check-code-attribution-tests.sh b/.claude/skills/check-code-attribution/validation-tests/check-code-attribution-tests.sh
new file mode 100755
index 00000000000..090acbe129b
--- /dev/null
+++ b/.claude/skills/check-code-attribution/validation-tests/check-code-attribution-tests.sh
@@ -0,0 +1,246 @@
+#!/usr/bin/env bash
+# check-code-attribution-tests.sh — Validate the check-code-attribution skill against synthetic scenarios.
+#
+# Usage:
+# ./check-code-attribution-tests.sh [--help]
+#
+# What it does:
+# 1. Validates EXPECTED.json and scenario fixtures (no API calls).
+# 2. Creates an isolated git worktree on a temp branch from HEAD.
+# 3. Creates a diff (non-isolated .java files, NOTICES catalog, mismatch snippet),
+# commits, and runs Warden on the main batch.
+# 4. Scenarios marked "isolated" in EXPECTED.json each get their own worktree and Warden
+# run to avoid prompt-cache priming that can suppress findings in concurrent batches.
+# 5. Asserts per-scenario pass/fail against EXPECTED.json (>= medium findings only).
+# 6. Prints Warden's actual output for each failing scenario.
+# 7. Cleans up all worktrees.
+#
+# Requires:
+# - Node.js / npx
+# - One of: WARDEN_ANTHROPIC_API_KEY, ANTHROPIC_API_KEY, or Pi OAuth config
+# (see validation-tests/README.md "Warden CLI" section for setup options)
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
+SCENARIOS_DIR="$SCRIPT_DIR/scenarios"
+CATALOG="$SCRIPT_DIR/THIRD_PARTY_NOTICES.catalog.md"
+EXPECTED_JSON="$SCRIPT_DIR/EXPECTED.json"
+VALIDATION="$SCRIPT_DIR/assert-scenarios.mjs"
+MISMATCH_SNIPPET="$SCENARIOS_DIR/THIRD_PARTY_NOTICES.mismatch-snippet.md"
+
+# Destination path inside the worktree — must not appear in warden.toml ignorePaths.
+DEST_PACKAGE_PATH="sentry/src/test/java/io/sentry/skills/verification"
+
+# Warden wall-clock limit (seconds).
+TIMEOUT_SEC=300
+
+die() { echo "ERROR: $*" >&2; exit 1; }
+
+show_usage() {
+ cat <<'EOF'
+Usage: check-code-attribution-tests.sh [--help]
+
+Validates the check-code-attribution skill against all scenarios in EXPECTED.json.
+Runs Warden on a temporary branch and asserts per-scenario pass/fail (>= medium findings).
+
+Prerequisites:
+ - Node.js (npx)
+ - API key: WARDEN_ANTHROPIC_API_KEY or ANTHROPIC_API_KEY
+ (or Pi OAuth: npx pi && /login — see README.md "Warden CLI" section)
+ - Wall-clock limit: gtimeout (brew install coreutils), GNU timeout, or perl
+EOF
+}
+
+[[ "${1:-}" == "--help" || "${1:-}" == "-h" ]] && { show_usage; exit 0; }
+
+# --- prereq checks ---
+
+command -v node >/dev/null 2>&1 || die "node not found — install Node.js."
+command -v npx >/dev/null 2>&1 || die "npx not found — install Node.js."
+command -v git >/dev/null 2>&1 || die "git not found."
+
+# macOS: GNU timeout is `gtimeout` from coreutils; fall back to perl alarm.
+TIMEOUT_CMD=()
+if command -v gtimeout >/dev/null 2>&1; then
+ TIMEOUT_CMD=(gtimeout "$TIMEOUT_SEC")
+elif command -v timeout >/dev/null 2>&1; then
+ TIMEOUT_CMD=(timeout "$TIMEOUT_SEC")
+elif command -v perl >/dev/null 2>&1; then
+ TIMEOUT_CMD=(perl -e 'alarm shift; exec @ARGV' "$TIMEOUT_SEC")
+else
+ die "Need gtimeout (brew install coreutils), GNU timeout, or perl for Warden wall-clock limit"
+fi
+
+if [[ -z "${WARDEN_ANTHROPIC_API_KEY:-}" && -z "${ANTHROPIC_API_KEY:-}" ]]; then
+ if [[ ! -f "$HOME/.pi/agent/auth.json" ]]; then
+ die "No API key found. Set WARDEN_ANTHROPIC_API_KEY, ANTHROPIC_API_KEY, or run: npx pi && /login"
+ fi
+fi
+
+node "$VALIDATION" validate "$EXPECTED_JSON" "$SCENARIOS_DIR"
+
+# --- cleanup tracking ---
+
+declare -a WORKTREES=()
+declare -a BRANCHES=()
+declare -a JSON_FILES=()
+
+cleanup() {
+ for wt in "${WORKTREES[@]+"${WORKTREES[@]}"}"; do
+ git -C "$REPO_ROOT" worktree remove --force "$wt" 2>/dev/null || true
+ done
+ for b in "${BRANCHES[@]+"${BRANCHES[@]}"}"; do
+ git -C "$REPO_ROOT" branch -D "$b" 2>/dev/null || true
+ done
+ (( ${#JSON_FILES[@]} )) && rm -f "${JSON_FILES[@]}"
+}
+trap cleanup EXIT
+
+# --- resolve base commit ---
+# Branch from HEAD so the worktree includes the current skill definition.
+
+BASE=$(git -C "$REPO_ROOT" rev-parse HEAD || die "Cannot resolve HEAD.")
+TS=$(date +%s)
+
+# --- helpers ---
+
+# Commits paths in a validation worktree with consistent author metadata.
+# Usage: git_commit_in_worktree [path...]
+git_commit_in_worktree() {
+ local worktree="$1" message="$2"
+ shift 2
+ if (($# > 0)); then
+ git -C "$worktree" add "$@"
+ fi
+ git -C "$worktree" \
+ -c user.email="ci@sentry.io" \
+ -c user.name="Validation Test" \
+ commit --quiet -m "$message"
+}
+
+# Creates a git worktree from $BASE and commits the NOTICES catalog as the Warden
+# analysis base — so only fixture changes appear in the diff Warden analyzes.
+# Prints the catalog-commit SHA to stdout.
+setup_catalog_base() {
+ local worktree="$1" branch="$2"
+ git -C "$REPO_ROOT" worktree add --quiet "$worktree" "$BASE" -b "$branch"
+ printf '\n' >> "$worktree/THIRD_PARTY_NOTICES.md"
+ sed "s|validation-tests/scenarios/|${DEST_PACKAGE_PATH}/|g" \
+ "$CATALOG" >> "$worktree/THIRD_PARTY_NOTICES.md"
+ git_commit_in_worktree "$worktree" "test: apply NOTICES catalog [skip ci]" \
+ THIRD_PARTY_NOTICES.md
+ git -C "$worktree" rev-parse HEAD
+}
+
+# Appends the mismatch snippet to THIRD_PARTY_NOTICES.md, stripping the fixture's
+# prose header so only the NOTICES entry itself lands in the file.
+append_mismatch_snippet() {
+ local worktree="$1"
+ printf '\n' >> "$worktree/THIRD_PARTY_NOTICES.md"
+ sed '1,/^---$/d' "$MISMATCH_SNIPPET" >> "$worktree/THIRD_PARTY_NOTICES.md"
+}
+
+# Runs Warden and writes JSON output to the given file.
+run_warden() {
+ local base="$1" worktree="$2" json_out="$3" label="$4"
+ echo "Running Warden on ${base:0:7}..HEAD ($label)..."
+ : > "$json_out"
+ if ! "${TIMEOUT_CMD[@]}" npx @sentry/warden "${base}..HEAD" \
+ --skill check-code-attribution \
+ --fail-on off \
+ --report-on medium \
+ --json \
+ -C "$worktree" \
+ > "$json_out"; then
+ if [[ ! -s "$json_out" ]]; then
+ die "Warden failed for $label with no JSON output (check API key, network, and Warden logs)."
+ fi
+ die "Warden exited with an error for $label but left partial JSON in $json_out."
+ fi
+ [[ -s "$json_out" ]] || die "Warden succeeded but produced no JSON output for $label."
+}
+
+# --- main worktree: non-isolated scenarios ---
+# Isolated .java files are omitted here; they get dedicated worktrees below.
+
+echo "Creating worktrees from $(git -C "$REPO_ROOT" rev-parse --short "$BASE")..."
+echo ""
+
+MAIN_WORKTREE=$(mktemp -d)
+MAIN_BRANCH="validation-main-${TS}"
+MAIN_JSON=$(mktemp)
+ROUTING_JSON_FILE=$(mktemp)
+echo '{}' > "$ROUTING_JSON_FILE"
+WORKTREES+=("$MAIN_WORKTREE")
+BRANCHES+=("$MAIN_BRANCH")
+JSON_FILES+=("$MAIN_JSON" "$ROUTING_JSON_FILE")
+
+MAIN_BASE=$(setup_catalog_base "$MAIN_WORKTREE" "$MAIN_BRANCH")
+
+DEST_DIR="$MAIN_WORKTREE/$DEST_PACKAGE_PATH"
+mkdir -p "$DEST_DIR"
+
+shopt -s nullglob
+copied=0
+while IFS= read -r java_file; do
+ cp "$SCENARIOS_DIR/$java_file" "$DEST_DIR/"
+ copied=$((copied + 1))
+done < <(node "$VALIDATION" list-main-java "$EXPECTED_JSON" "$SCENARIOS_DIR")
+echo "Copied ${copied} scenario files → $DEST_PACKAGE_PATH/ (non-isolated batch)"
+append_mismatch_snippet "$MAIN_WORKTREE"
+git_commit_in_worktree "$MAIN_WORKTREE" \
+ "test: add check-code-attribution validation fixtures [skip ci]" \
+ "$DEST_PACKAGE_PATH" THIRD_PARTY_NOTICES.md
+
+run_warden "$MAIN_BASE" "$MAIN_WORKTREE" "$MAIN_JSON" "main"
+node "$VALIDATION" routing-set "$ROUTING_JSON_FILE" main "$MAIN_JSON"
+
+# --- isolated worktrees: one per scenario marked "isolated" in EXPECTED.json ---
+#
+# Scenarios where Anthropic prompt-cache priming can suppress findings in a concurrent
+# batch get their own worktree and Warden run. EXPECTED.json is the single source of
+# truth for which scenarios need isolation — add "isolated": true there, not here.
+# Java isolates omit the mismatch snippet; the NOTICES mismatch scenario adds it alone.
+
+while IFS=$'\t' read -r id file; do
+ worktree=$(mktemp -d)
+ branch="validation-isolated-${TS}-${id//[^a-zA-Z0-9]/-}"
+ json=$(mktemp)
+ WORKTREES+=("$worktree")
+ BRANCHES+=("$branch")
+ JSON_FILES+=("$json")
+
+ base=$(setup_catalog_base "$worktree" "$branch")
+
+ commit_paths=()
+ if [[ "$file" == *.java ]]; then
+ dest_dir="$worktree/$DEST_PACKAGE_PATH"
+ mkdir -p "$dest_dir"
+ cp "$SCENARIOS_DIR/$file" "$dest_dir/"
+ commit_paths=("$DEST_PACKAGE_PATH")
+ elif [[ "$file" == "THIRD_PARTY_NOTICES.md" ]]; then
+ append_mismatch_snippet "$worktree"
+ commit_paths=(THIRD_PARTY_NOTICES.md)
+ else
+ die "Unsupported isolated scenario file: $file (id: $id)"
+ fi
+
+ git_commit_in_worktree "$worktree" "test: isolated fixture for $id [skip ci]" \
+ "${commit_paths[@]}"
+
+ echo ""
+ run_warden "$base" "$worktree" "$json" "$id"
+ node "$VALIDATION" routing-set "$ROUTING_JSON_FILE" "$id" "$json"
+
+done < <(node "$VALIDATION" list-isolated "$EXPECTED_JSON")
+
+echo ""
+echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
+
+# --- assert per-scenario ---
+#
+# ROUTING_JSON_FILE maps scenario id → Warden JSONL path; non-isolated scenarios use "main".
+
+node "$VALIDATION" assert "$EXPECTED_JSON" "$DEST_PACKAGE_PATH" "$ROUTING_JSON_FILE"
diff --git a/.claude/skills/check-code-attribution/validation-tests/scenarios/HeaderCompleteAndNoticePresent.java b/.claude/skills/check-code-attribution/validation-tests/scenarios/HeaderCompleteAndNoticePresent.java
new file mode 100644
index 00000000000..63727be1d5c
--- /dev/null
+++ b/.claude/skills/check-code-attribution/validation-tests/scenarios/HeaderCompleteAndNoticePresent.java
@@ -0,0 +1,19 @@
+/*
+ * Adapted from https://github.com/example/something
+ *
+ * Copyright 2020 Example Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ */
+package io.sentry.skills.verification;
+
+public final class HeaderCompleteAndNoticePresent {
+
+ public int sum(int a, int b) {
+ return a + b;
+ }
+}
diff --git a/.claude/skills/check-code-attribution/validation-tests/scenarios/HeaderCompleteButNoticeMissing.java b/.claude/skills/check-code-attribution/validation-tests/scenarios/HeaderCompleteButNoticeMissing.java
new file mode 100644
index 00000000000..081d1848300
--- /dev/null
+++ b/.claude/skills/check-code-attribution/validation-tests/scenarios/HeaderCompleteButNoticeMissing.java
@@ -0,0 +1,17 @@
+/*
+ * Adapted from https://github.com/example
+ *
+ * Copyright 2024 Example Authors
+ *
+ * Licensed under the MIT License
+ *
+ * https://github.com/example/something
+ */
+package io.sentry.skills.verification;
+
+public final class HeaderCompleteButNoticeMissing {
+
+ public boolean ok() {
+ return true;
+ }
+}
diff --git a/.claude/skills/check-code-attribution/validation-tests/scenarios/HeaderFullyStripped.java b/.claude/skills/check-code-attribution/validation-tests/scenarios/HeaderFullyStripped.java
new file mode 100644
index 00000000000..6973848c61e
--- /dev/null
+++ b/.claude/skills/check-code-attribution/validation-tests/scenarios/HeaderFullyStripped.java
@@ -0,0 +1,7 @@
+/* Attribution stripped — fixture for check-code-attribution validation only. */
+package io.sentry.skills.verification;
+
+public final class HeaderFullyStripped {
+
+ public void run() {}
+}
diff --git a/.claude/skills/check-code-attribution/validation-tests/scenarios/HeaderMissingButNoticePresent.java b/.claude/skills/check-code-attribution/validation-tests/scenarios/HeaderMissingButNoticePresent.java
new file mode 100644
index 00000000000..5c4953ea3ad
--- /dev/null
+++ b/.claude/skills/check-code-attribution/validation-tests/scenarios/HeaderMissingButNoticePresent.java
@@ -0,0 +1,8 @@
+package io.sentry.skills.verification;
+
+public final class HeaderMissingButNoticePresent {
+
+ public int compute(int x) {
+ return x * 2;
+ }
+}
diff --git a/.claude/skills/check-code-attribution/validation-tests/scenarios/HeaderMissingNonEssentialInfo.java b/.claude/skills/check-code-attribution/validation-tests/scenarios/HeaderMissingNonEssentialInfo.java
new file mode 100644
index 00000000000..c524a2593a4
--- /dev/null
+++ b/.claude/skills/check-code-attribution/validation-tests/scenarios/HeaderMissingNonEssentialInfo.java
@@ -0,0 +1,12 @@
+// Adapted from ExampleLib.
+// Copyright 2020 Example Corp.
+// Licensed under the MIT License.
+// https://github.com/example/examplelib
+package io.sentry.skills.verification;
+
+public final class HeaderMissingNonEssentialInfo {
+
+ public int compute(int x) {
+ return x + 1;
+ }
+}
diff --git a/.claude/skills/check-code-attribution/validation-tests/scenarios/HeaderPartiallyStripped.java b/.claude/skills/check-code-attribution/validation-tests/scenarios/HeaderPartiallyStripped.java
new file mode 100644
index 00000000000..0389934d94a
--- /dev/null
+++ b/.claude/skills/check-code-attribution/validation-tests/scenarios/HeaderPartiallyStripped.java
@@ -0,0 +1,10 @@
+// Adapted from Example RateLimiter.
+// https://github.com/example
+package io.sentry.skills.verification;
+
+public final class HeaderPartiallyStripped {
+
+ public synchronized boolean tryAcquire() {
+ return true;
+ }
+}
diff --git a/.claude/skills/check-code-attribution/validation-tests/scenarios/NewLicenseType.java b/.claude/skills/check-code-attribution/validation-tests/scenarios/NewLicenseType.java
new file mode 100644
index 00000000000..e148f5a1a4f
--- /dev/null
+++ b/.claude/skills/check-code-attribution/validation-tests/scenarios/NewLicenseType.java
@@ -0,0 +1,10 @@
+// Adapted from ExampleLib.
+// Copyright 2020 Example Corp.
+// Licensed under the GNU Affero General Public License v3.0.
+// https://github.com/example/agpl-lib
+package io.sentry.skills.verification;
+
+public final class NewLicenseType {
+
+ public void run() {}
+}
diff --git a/.claude/skills/check-code-attribution/validation-tests/scenarios/THIRD_PARTY_NOTICES.mismatch-snippet.md b/.claude/skills/check-code-attribution/validation-tests/scenarios/THIRD_PARTY_NOTICES.mismatch-snippet.md
new file mode 100644
index 00000000000..5a9b87285df
--- /dev/null
+++ b/.claude/skills/check-code-attribution/validation-tests/scenarios/THIRD_PARTY_NOTICES.mismatch-snippet.md
@@ -0,0 +1,37 @@
+# Snippet fixture — MismatchLib entry for the isolated mismatch worktree.
+# header-vs-notice-mismatch: copyright in metadata field does not match embedded license text.
+
+---
+
+## Example — MismatchLib (MIT)
+
+**Source:** https://github.com/example/mismatch
+**License:** MIT License
+**Copyright:** Copyright (c) 2020 Wrong Holder
+
+### Scope
+
+Validation sample only. The code resides in `io.sentry.skills.verification.MismatchLib`.
+
+```
+MIT License
+
+Copyright (c) 2016 Correct Holder
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+```
diff --git a/.claude/skills/create-java-pr/SKILL.md b/.claude/skills/create-java-pr/SKILL.md
new file mode 100644
index 00000000000..17b5839d88a
--- /dev/null
+++ b/.claude/skills/create-java-pr/SKILL.md
@@ -0,0 +1,217 @@
+---
+name: create-java-pr
+description: Create a pull request in sentry-java. Use when asked to "create pr", "prepare pr", "prep pr", "open pr", "ready for pr", "prepare for review", "finalize changes". Handles branch creation, code formatting, API dump, committing, pushing, PR creation, changelog, and stacked PRs.
+---
+
+# Create Pull Request (sentry-java)
+
+Prepare local changes and create a pull request for the sentry-java repo.
+
+**For stacked PRs:** read `references/stacked-prs.md` before proceeding. It is the source of truth for
+stack structure, title naming, stack list format, and merge strategy.
+
+## Step 0: Determine PR Type From Git Branch Context
+
+Infer PR type from the current branch before asking the user.
+
+1. Get current branch:
+
+```bash
+git branch --show-current
+```
+
+2. Apply these rules:
+
+- **If branch is `main` or `master`**: default to a **standalone PR**.
+ - Do **not** assume stack mode from `main`.
+ - Only use stack mode if the user explicitly asks for a stacked PR.
+- **If branch is not `main`/`master`**:
+ - Check whether that branch already has a PR and what its base is:
+ ```bash
+ gh pr list --head "$(git branch --show-current)" --json number,baseRefName,title --jq '.[0]'
+ ```
+ - If that branch PR exists and `baseRefName` is **not** `main`/`master`, treat the work as a **stacked PR context**.
+ - If that branch PR exists and `baseRefName` **is** `main`/`master`, also check whether other PRs target the current branch:
+ ```bash
+ gh pr list --base "$(git branch --show-current)" --json number,headRefName,title
+ ```
+ - If there are downstream PRs, treat this as **next PR in an existing stack** with the current branch as the stack base (collection branch).
+ - If there are no downstream PRs, treat it as **standalone PR context**.
+ - If no PR exists for the current branch, check whether other PRs target it:
+ ```bash
+ gh pr list --base "$(git branch --show-current)" --json number,headRefName,title
+ ```
+ - If there are downstream PRs, treat this as **next PR in an existing stack** with the current branch as the stack base (collection branch).
+ - If there are no downstream PRs either, treat it as **standalone PR context** (fresh feature branch).
+
+3. If signals are mixed or ambiguous, ask one focused question to confirm.
+
+PR types:
+- **Standalone PR** — regular PR targeting `main`.
+- **First PR of a new stack** — create collection branch from `main`, then first PR off it.
+- **Next PR in an existing stack** — target the current stack base branch (usually the previous stack PR branch, or the collection branch if creating the first follow-up PR from the collection branch).
+
+If the user explicitly says "stack", "stacked PR", or provides numbered stack titles (e.g. `[Topic 2]`), honor that even if branch heuristics are inconclusive.
+
+## Step 1: Ensure Feature Branch
+
+```bash
+git branch --show-current
+```
+
+If on `main` or `master`, create and switch to a new branch:
+
+```bash
+git checkout -b /
+```
+
+Derive the branch name from the changes being made. Use `feat/`, `fix/`, `ref/`, etc. matching the commit type conventions.
+
+**For stacked PRs:** For the first PR in a new stack, first create and push the collection branch (see `references/stacked-prs.md` § "Why a Collection Branch"), then branch the PR off it. For subsequent PRs, branch off the previous stack branch. Give every branch in the stack a shared prefix naming the feature, with a descriptive suffix per PR.
+
+**CRITICAL: Never merge, fast-forward, or push commits into the collection branch.** It stays at its initial position until the user merges stack PRs through GitHub. Updating it will auto-merge and destroy the entire PR stack.
+
+## Step 2: Format Code and Regenerate API Files
+
+```bash
+./gradlew spotlessApply apiDump
+```
+
+This is **required** before every PR in this repo. It formats all Java/Kotlin code via Spotless and regenerates the `.api` binary compatibility files.
+
+If the command fails, diagnose and fix the issue before continuing.
+
+## Step 3: Commit Changes
+
+Check for uncommitted changes:
+
+```bash
+git status --porcelain
+```
+
+If there are uncommitted changes, invoke the `sentry-skills:commit` skill to stage and commit them following [Sentry commit message conventions](https://develop.sentry.dev/engineering-practices/commit-messages/):
+
+```
+():
+```
+
+Allowed types: `feat`, `fix`, `ref`, `chore`, `docs`, `test`, `perf`, `build`, `ci`, `style`, `meta`, `license`
+
+**Important:** When staging, ignore changes that are only relevant for local testing and should not be part of the PR. Common examples:
+
+| Ignore Pattern | Reason |
+|---|---|
+| Hardcoded booleans flipped for testing | Local debug toggles |
+| Sample app config changes (`sentry-samples/`) | Local testing configuration |
+| `.env` or credentials files | Secrets |
+
+Restore these files before committing:
+
+```bash
+git checkout --
+```
+
+## Step 4: Push the Branch
+
+```bash
+git push -u origin HEAD
+```
+
+If the push fails due to diverged history, ask the user how to proceed rather than force-pushing.
+
+## Step 5: Create PR
+
+Invoke the `sentry-skills:create-pr` skill to create a draft PR.
+
+Read `.github/pull_request_template.md` and use it as the PR body structure — it is the single source
+of truth for the sections and checklist, so never reproduce it from memory. Fill in each section based
+on the changes being PR'd, drop the HTML comment hints, and check any checklist items that apply.
+
+**PR title format** — same as the commit subject (Step 3):
+
+```
+():
+```
+
+Examples:
+- `feat(core): Add structured logging support`
+- `fix(android): Prevent crash on API 21 when registering receiver`
+
+**For stacked PRs:**
+
+- Pass `--base ` so the PR targets the previous branch (first PR in a stack targets the collection branch).
+- Use the stacked PR title format: `(): [ ] ` (see `references/stacked-prs.md` § "PR Title Naming").
+- Include the stack list at the top of the PR body, before the `## :scroll: Description` section (see `references/stacked-prs.md` § "Stack List in PR Description" for the format).
+- Add a merge method reminder at the very end of the PR body (see `references/stacked-prs.md` § "Stack List in PR Description" for the exact text). This only applies to stack PRs, not the collection branch PR.
+
+Then continue to Step 5.5 (stacked PRs only) or Step 6.
+
+## Step 5.5: Update Stack List on All PRs (stacked PRs only)
+
+Skip this step for standalone PRs.
+
+After creating the PR, update the PR description on **every other PR in the stack — including the collection branch PR** — so all PRs have the same up-to-date stack list. Follow the format and commands in `references/stacked-prs.md` § "Stack List in PR Description".
+
+Edit each body using the procedure in § "Editing PR Descriptions" below.
+
+## Step 6: Update Changelog
+
+First, determine whether a changelog entry is needed. **Skip this step** (and go straight to "No changelog needed" below) if the changes are not user-facing, for example:
+
+- Test-only changes (new tests, test refactors, test fixtures)
+- CI/CD or build configuration changes
+- Documentation-only changes
+- Code comments or formatting-only changes
+- Internal refactors with no behavior change visible to SDK users
+- Sample app changes
+
+If unsure, ask the user.
+
+### If changelog is needed
+
+Add an entry to `CHANGELOG.md` under the `## Unreleased` section.
+
+#### Determine the subsection
+
+| Change Type | Subsection |
+|---|---|
+| New feature | `### Features` |
+| Bug fix | `### Fixes` |
+| Refactoring, internal cleanup | `### Internal` |
+| Dependency update | `### Dependencies` |
+
+Create the subsection under `## Unreleased` if it does not already exist.
+
+**When rebasing:** A rebase onto `main` can land your branch after a release was cut, where the `## Unreleased` heading your entry lived under has since been renamed to that version number. If that happens, move your new entry into an `## Unreleased` section at the top of `CHANGELOG.md` (create the section if it no longer exists) so it is not left under an already-released version.
+
+#### Entry format
+
+```markdown
+- ([#](https://github.com/getsentry/sentry-java/pull/))
+```
+
+Use the PR number returned by `sentry-skills:create-pr`. Match the style of existing entries — sentence case, ending with the PR link, no trailing period.
+
+#### Commit and push
+
+Stage `CHANGELOG.md`, commit with message `changelog`, and push:
+
+```bash
+git add CHANGELOG.md
+git commit -m "changelog"
+git push
+```
+
+### No changelog needed
+
+If no changelog entry is needed, append `#skip-changelog` to the end of the PR description to disable
+the changelog CI check, using the procedure in § "Editing PR Descriptions" below.
+
+## Editing PR Descriptions
+
+Do not use shell redirects (`>`, `>>`), pipes (`|`), or compound commands (`&&`, `||`). These create
+compound shell expressions that won't match permission patterns. Instead:
+
+1. Read the body with `gh pr view --json body --jq '.body'` (output is returned directly)
+2. Use the `Write` tool to save it to `/tmp/pr-body.md`, and the `Edit` tool to modify it
+3. Update with `gh pr edit --body-file /tmp/pr-body.md`
diff --git a/.claude/skills/create-java-pr/references/stacked-prs.md b/.claude/skills/create-java-pr/references/stacked-prs.md
new file mode 100644
index 00000000000..56221fe4912
--- /dev/null
+++ b/.claude/skills/create-java-pr/references/stacked-prs.md
@@ -0,0 +1,86 @@
+# Stacked PRs
+
+Stacked PRs split a large feature into small, easy-to-review PRs where each builds on the previous
+one. The general mechanics are the standard [Graphite](https://graphite.dev/) stacking workflow —
+this file covers only what is specific to sentry-java.
+
+## Why a Collection Branch
+
+```
+main ← collection-branch ← stack-pr-1 ← stack-pr-2 ← stack-pr-3 ← ...
+```
+
+A **collection branch** is created from `main` and targets `main`. The first stack PR targets it
+rather than `main`, and each later PR targets the previous stack PR's branch.
+
+It exists because PRs targeting `main` are **squash**-merged, which causes repeated merge conflicts
+when syncing a stack. Stack PRs are therefore **merge-committed** into the collection branch, and
+only the collection branch is squash-merged into `main` at the end — giving `main` one clean commit
+for the whole feature.
+
+Create it with an empty commit, so GitHub allows opening a PR:
+
+```bash
+git commit --allow-empty -m "collection: "
+```
+
+Push it and open its PR against `main` right away — it is the PR the whole stack is eventually
+squash-merged through, and it carries the stack list like every other PR. Give it a plain title
+(`(): `, no `[ ]` bracket) and no merge method reminder.
+
+## Rules That Will Destroy a Stack If Broken
+
+**Never update the collection branch yourself.** Never merge, fast-forward, or push stack branch
+commits into it. It stays at its initial position (the empty commit on `main`) until the user merges
+stack PRs through GitHub one by one. Fast-forwarding it makes GitHub auto-merge and delete every
+stack PR branch, destroying the entire stack.
+
+**Never amend or force-push a stack branch.** No `git commit --amend`, `--force`, or
+`--force-with-lease` on a branch that is part of a stack — a force-push can cause GitHub to
+auto-merge or auto-close the other PRs in the stack. If a commit needs fixing, add a fixup commit.
+
+**Sync only between adjacent stack branches**, by merging forward — never into the collection branch.
+Prefer merge over rebase; only rebase if explicitly requested.
+
+**Do not merge PRs.** Only the user merges them, bottom to top.
+
+## PR Title Naming
+
+Include the topic name and a sequential number in brackets:
+
+```
+(): [ ]
+```
+
+Examples:
+- `feat(core): [Global Attributes 1] Add scope-level attributes API`
+- `feat(core): [Global Attributes 2] Wire scope attributes into LoggerApi and MetricsApi`
+
+## Stack List in PR Description
+
+Every PR in the stack — **including the collection branch PR** — must have a stack list **at the top
+of its description**, before the `## :scroll: Description` section. When a PR is added, update the
+description on **all** PRs in the stack. The stack list is also how you enumerate a stack: read it
+off any PR body rather than guessing from branch names, which may use different prefixes.
+
+```markdown
+## PR Stack ()
+
+- #5118
+- #5120
+- #5121
+
+---
+```
+
+No status column — GitHub already shows that. The `---` separates the stack list from the rest of
+the description.
+
+**Merge method reminder:** on stack PRs (not the collection branch PR), end the description with:
+
+```markdown
+> ⚠️ **Merge this PR using a merge commit** (not squash). Only the collection branch is squash-merged into main.
+```
+
+Updating every PR's stack list means editing several descriptions — follow the procedure in
+`SKILL.md` § "Editing PR Descriptions".
diff --git a/.claude/skills/test/SKILL.md b/.claude/skills/test/SKILL.md
new file mode 100644
index 00000000000..7e6ddd37294
--- /dev/null
+++ b/.claude/skills/test/SKILL.md
@@ -0,0 +1,85 @@
+---
+name: test
+description: Run tests for a specific SDK module. Use when asked to "run tests", "test module", "run unit tests", "run system tests", "run e2e tests", or test a specific class. Auto-detects unit vs system tests. Supports interactive mode.
+allowed-tools: Bash, Read, Glob, AskUserQuestion
+argument-hint: [interactive] [test-class-filter]
+---
+
+# Run Tests
+
+Run tests for a specific module. Auto-detects whether to run unit tests or system tests.
+
+## Step 0: Check for Interactive Mode
+
+If `$ARGUMENTS` starts with `interactive` (e.g., `/test interactive sentry ScopesTest`), enable interactive mode. Strip the `interactive` keyword from the arguments before proceeding.
+
+In interactive mode, use AskUserQuestion at decision points as described in the steps below.
+
+## Step 1: Parse the Argument
+
+The argument can be either:
+- A **file path** (e.g., `@sentry/src/test/java/io/sentry/ScopesTest.kt`)
+- A **module name** (e.g., `sentry-android-core`, `sentry-samples-spring-boot-4`)
+- A **module name + test filter** (e.g., `sentry ScopesTest`)
+
+Extract the module name and optional test class filter from the argument.
+
+**Interactive mode:** If the test filter is ambiguous (e.g., matches multiple test classes across modules), use AskUserQuestion to let the user pick which test class(es) to run.
+
+## Step 2: Detect Test Type
+
+| Signal | Test Type |
+|--------|-----------|
+| Path contains `sentry-samples/` | System test |
+| Module name starts with `sentry-samples-` | System test |
+| Everything else | Unit test |
+
+## Step 3a: Run Unit Tests
+
+Determine the Gradle test task:
+
+| Module Pattern | Test Task |
+|---------------|-----------|
+| `sentry-android-*` | `testReleaseUnitTest` |
+| `sentry-compose*` | `testReleaseUnitTest` |
+| `*-android` | `testReleaseUnitTest` |
+| Everything else | `test` |
+
+**Interactive mode:** Before running, read the test class file and use AskUserQuestion to ask:
+- "Run all tests in this class, or a specific method?" — list the test method names as options.
+
+If the user picks a specific method, use `--tests="*ClassName.methodName"` as the filter.
+
+With a test class filter:
+```bash
+./gradlew '::' --tests="**" --info
+```
+
+Without a filter:
+```bash
+./gradlew '::' --info
+```
+
+## Step 3b: Run System Tests
+
+System tests require the Python-based test runner which manages a mock Sentry server and sample app lifecycle.
+
+1. Ensure the Python venv exists:
+```bash
+test -d .venv || make setupPython
+```
+
+2. Extract the sample module name. For file paths like `sentry-samples//src/...`, the sample module is the directory name (e.g., `sentry-samples-spring`).
+
+3. Run the system test:
+```bash
+.venv/bin/python test/system-test-runner.py test --module
+```
+
+This starts the mock Sentry server, starts the sample app (Spring Boot/Tomcat/CLI), runs tests via `./gradlew :sentry-samples::systemTest`, and cleans up afterwards.
+
+## Step 4: Report Results
+
+Summarize the test outcome:
+- Total tests run, passed, failed, skipped
+- For failures: show the failing test name and the assertion/error message
diff --git a/.craft.yml b/.craft.yml
index 7dbf0382589..bee668917c1 100644
--- a/.craft.yml
+++ b/.craft.yml
@@ -34,20 +34,30 @@ targets:
maven:io.sentry:sentry-apache-http-client-5:
maven:io.sentry:sentry-android:
maven:io.sentry:sentry-android-core:
+ maven:io.sentry:sentry-android-distribution:
maven:io.sentry:sentry-android-ndk:
maven:io.sentry:sentry-android-timber:
maven:io.sentry:sentry-kotlin-extensions:
maven:io.sentry:sentry-android-fragment:
maven:io.sentry:sentry-bom:
maven:io.sentry:sentry-openfeign:
+ maven:io.sentry:sentry-openfeature:
+ maven:io.sentry:sentry-launchdarkly-android:
+ maven:io.sentry:sentry-launchdarkly-server:
maven:io.sentry:sentry-opentelemetry-agent:
+ # TODO: Add after first release of the artifact.
+ # maven:io.sentry:sentry-opentelemetry-bom:
maven:io.sentry:sentry-opentelemetry-agentcustomization:
maven:io.sentry:sentry-opentelemetry-agentless:
maven:io.sentry:sentry-opentelemetry-agentless-spring:
maven:io.sentry:sentry-opentelemetry-bootstrap:
maven:io.sentry:sentry-opentelemetry-core:
+ maven:io.sentry:sentry-opentelemetry-otlp:
+ maven:io.sentry:sentry-opentelemetry-otlp-spring:
+ maven:io.sentry:sentry-kafka:
maven:io.sentry:sentry-apollo:
maven:io.sentry:sentry-jdbc:
+ maven:io.sentry:sentry-jcache:
maven:io.sentry:sentry-graphql:
maven:io.sentry:sentry-graphql-22:
maven:io.sentry:sentry-graphql-core:
@@ -63,3 +73,5 @@ targets:
maven:io.sentry:sentry-apollo-4:
maven:io.sentry:sentry-reactor:
maven:io.sentry:sentry-ktor-client:
+ maven:io.sentry:sentry-async-profiler:
+ maven:io.sentry:sentry-spotlight:
diff --git a/.cursor/rules/api.mdc b/.cursor/rules/api.mdc
new file mode 100644
index 00000000000..c5a793d9240
--- /dev/null
+++ b/.cursor/rules/api.mdc
@@ -0,0 +1,89 @@
+---
+alwaysApply: false
+description: Public API surface, binary compatibility, and common classes to modify
+---
+# Java SDK Public API
+
+## API Compatibility
+
+Public API is tracked via `.api` files generated by the [Binary Compatibility Validator](https://github.com/Kotlin/binary-compatibility-validator) Gradle plugin. Each module has its own file at `/api/.api`.
+
+- **Never edit `.api` files manually.** Run `./gradlew apiDump` to regenerate them.
+- `./gradlew check` validates current code against `.api` files and fails on unintended changes.
+- `@ApiStatus.Internal` marks classes/methods as internal — they still appear in `.api` files but are not part of the public contract.
+- `@ApiStatus.Experimental` marks API that may change in future versions.
+
+## Key Public API Classes
+
+### Entry Point
+
+`Sentry` (`sentry` module) is the static entry point. Most public API methods on `Sentry` delegate to `getCurrentScopes()`. When adding a new method to `Sentry`, it typically calls through to `IScopes`.
+
+### Interfaces
+
+| Interface | Description |
+|-----------|-------------|
+| `IScope` | Single scope — holds data (tags, extras, breadcrumbs, attributes, user, contexts, etc.) |
+| `IScopes` | Multi-scope container — manages global, isolation, and current scope; delegates capture calls to `SentryClient` |
+| `ISpan` | Performance span — timing, tags, data, measurements |
+| `ITransaction` | Top-level transaction — extends `ISpan` |
+
+### Configuration
+
+`SentryOptions` is the base configuration class. Platform-specific subclasses:
+- `SentryAndroidOptions` — Android-specific options
+- Integration modules may add their own (e.g. `SentrySpringProperties`)
+
+New features must be **opt-in by default** — add a getter/setter pair to the appropriate options class.
+
+### Internal Classes (Not Public API)
+
+| Class | Description |
+|-------|-------------|
+| `SentryClient` | Sends events/envelopes to Sentry — receives captured data from `Scopes` |
+| `SentryEnvelope` / `SentryEnvelopeItem` | Low-level envelope serialization |
+| `Scope` | Concrete implementation of `IScope` |
+| `Scopes` | Concrete implementation of `IScopes` |
+
+## Adding New Public API
+
+When adding a new method that users can call (e.g. a new scope operation), these classes typically need changes:
+
+### Interfaces and Static API
+1. `IScope` — add the method signature
+2. `IScopes` — add the method signature (usually delegates to a scope)
+3. `Sentry` — add static method that calls `getCurrentScopes()`
+
+### Implementations
+4. `Scope` — actual implementation with data storage
+5. `Scopes` — delegates to the appropriate scope (global, isolation, or current based on `defaultScopeType`)
+6. `CombinedScopeView` — defines how the three scope types combine for reads (merge, first-wins, or specific scope)
+
+### No-Op and Adapter Classes
+7. `NoOpScope` — no-op stub for `IScope`
+8. `NoOpScopes` — no-op stub for `IScopes`
+9. `ScopesAdapter` — delegates to `Sentry` static API
+10. `HubAdapter` — deprecated bridge from old `IHub` API
+11. `HubScopesWrapper` — wraps `IScopes` as `IHub`
+
+### Serialization (if the data is sent to Sentry)
+12. Add serialization/deserialization in the relevant data class or create a new one implementing `JsonSerializable` and `JsonDeserializer`
+
+### Tests
+13. Write tests for all implementations, especially `Scope`, `Scopes`, `SentryTest`, and any new data classes
+14. No-op classes typically don't need separate tests unless they have non-trivial logic
+
+## Protocol / Data Model Classes
+
+Classes in the `io.sentry.protocol` package represent the Sentry event protocol. They implement `JsonSerializable` for serialization and have a companion `Deserializer` class implementing `JsonDeserializer`. When adding new fields to protocol classes, update both serialization and deserialization.
+
+## Namespaced APIs
+
+Newer features are namespaced under `Sentry.()` rather than added directly to `Sentry`. Each namespaced API has an interface, implementation, and no-op. Examples:
+
+- `Sentry.logger()` → `ILoggerApi` / `LoggerApi` / `NoOpLoggerApi` (structured logging, `io.sentry.logger` package)
+- `Sentry.metrics()` → `IMetricsApi` / `MetricsApi` / `NoOpMetricsApi` (metrics)
+
+Options for namespaced features are similarly nested under `SentryOptions`, e.g. `SentryOptions.getMetrics()`, `SentryOptions.getLogs()`.
+
+These APIs may share infrastructure like the type system (`SentryAttributeType.inferFrom()`) — changes to shared components (e.g. attribute types) may require updates across multiple namespaced APIs.
diff --git a/.cursor/rules/coding.mdc b/.cursor/rules/coding.mdc
deleted file mode 100644
index e7af7273f15..00000000000
--- a/.cursor/rules/coding.mdc
+++ /dev/null
@@ -1,53 +0,0 @@
----
-alwaysApply: true
-description: Cursor Coding Rules
----
-
-# Contributing Rules for Agents
-
-## Overview
-
-sentry-java is the Java and Android SDK for Sentry. This repository contains the source code and examples for SDK usage.
-
-## Tech Stack
-
-- **Language**: Java and Kotlin
-- **Build Framework**: Gradle
-
-## Key Commands
-
-```bash
-# Format code and regenerate .api files
-./gradlew spotlessApply apiDump
-
-# Run all tests and linter
-./gradlew check
-
-# Run unit tests for a specific file
-./gradle '::testDebugUnitTest' --tests="**" --info
-```
-
-## Contributing Guidelines
-
-1. Follow existing code style and language
-2. Do not modify the API files (e.g. sentry.api) manually, instead run `./gradlew apiDump` to regenerate them
-3. Write comprehensive tests
-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
-5. Consider backwards compatibility
-
-## Coding rules
-
-1. First think through the problem, read the codebase for relevant files, and propose a plan
-2. Before you begin working, check in with me and I will verify the plan
-3. Then, begin working on the todo items, marking them as complete as you go
-4. Please do not describe every step of the way and just give me a high level explanation of what changes you made
-5. Make every task and code change you do as simple as possible. We want to avoid making any massive or complex changes. Every change should impact as little code as possible. Everything is about simplicity.
-6. Once you're done, format the code and regenerate the .api files using the following command `./gradlew spotlessApply apiDump`
-7. As a last step, git stage the relevant files and propose (but not execute) a single git commit command (e.g. `git commit -m ""`)
-
-
-## Useful Resources
-
-- Main SDK documentation: https://develop.sentry.dev/sdk/overview/
-- Internal contributing guide: https://docs.sentry.io/internal/contributing/
-- Git commit messages conventions: https://develop.sentry.dev/engineering-practices/commit-messages/
diff --git a/.cursor/rules/continuous_profiling_jvm.mdc b/.cursor/rules/continuous_profiling_jvm.mdc
new file mode 100644
index 00000000000..d9a911de25e
--- /dev/null
+++ b/.cursor/rules/continuous_profiling_jvm.mdc
@@ -0,0 +1,174 @@
+---
+alwaysApply: false
+description: JVM Continuous Profiling (sentry-async-profiler)
+---
+# JVM Continuous Profiling
+
+Use this rule when working on JVM continuous profiling in `sentry-async-profiler` and the related core profiling abstractions in `sentry`.
+
+This area is suitable for LLM work, but do not rely on this rule alone for behavior changes. Always read the implementation and nearby tests first, especially for sampling, lifecycle, rate limiting, and file cleanup behavior.
+
+## Module Structure
+
+- **`sentry-async-profiler`**: standalone module containing the async-profiler integration
+ - Uses Java `ServiceLoader` discovery
+ - No direct dependency from core `sentry` module
+ - Enabled by adding the module as a dependency
+
+- **`sentry` core abstractions**:
+ - `IContinuousProfiler`: profiler lifecycle interface
+ - `ProfileChunk`: profile chunk payload sent to Sentry
+ - `IProfileConverter`: converts JVM JFR files into `SentryProfile`
+ - `ProfileLifecycle`: controls MANUAL vs TRACE lifecycle
+ - `ProfilingServiceLoader`: loads profiler and converter implementations via `ServiceLoader`
+
+## Key Classes
+
+### `JavaContinuousProfiler` (`sentry-async-profiler`)
+- Wraps the native async-profiler library
+- Writes JFR files to `profilingTracesDirPath`
+- Rotates chunks periodically via `MAX_CHUNK_DURATION_MILLIS` (currently 10s)
+- Implements `RateLimiter.IRateLimitObserver`
+- Maintains `rootSpanCounter` for TRACE lifecycle
+- Keeps a session-level `profilerId` across chunks until the profiling session ends
+- `getChunkId()` currently returns `SentryId.EMPTY_ID`, but emitted `ProfileChunk`s get a fresh chunk id when built in `stop(...)`
+
+### `ProfileChunk`
+- Carries `profilerId`, `chunkId`, timestamp, platform, measurements, and a JFR file reference
+- Built via `ProfileChunk.Builder`
+- For JVM, the JFR file is converted later during envelope item creation, not inside `JavaContinuousProfiler`
+
+### `ProfileLifecycle`
+- `MANUAL`: explicit `Sentry.startProfiler()` / `Sentry.stopProfiler()`
+- `TRACE`: profiler lifecycle follows active sampled root spans
+
+## Configuration
+
+Continuous profiling is **not** controlled by `profilesSampleRate`.
+
+Key options:
+- **`profileSessionSampleRate`**: session-level sample rate for continuous profiling
+- **`profileLifecycle`**: `ProfileLifecycle.MANUAL` (default) or `ProfileLifecycle.TRACE`
+- **`cacheDirPath`**: base SDK cache directory; profiling traces are written under the derived `profilingTracesDirPath`
+- **`profilingTracesHz`**: sampling frequency in Hz (default: 101)
+
+Continuous profiling is enabled when:
+- `profilesSampleRate == null`
+- `profilesSampler == null`
+- `profileSessionSampleRate != null && profileSessionSampleRate > 0`
+
+Example:
+
+```java
+options.setProfileSessionSampleRate(1.0);
+options.setCacheDirPath("/tmp/sentry-cache");
+options.setProfileLifecycle(ProfileLifecycle.MANUAL);
+options.setProfilingTracesHz(101);
+```
+
+## How It Works
+
+### Initialization
+- `InitUtil.initializeProfiler(...)` resolves or creates the profiling traces directory
+- `ProfilingServiceLoader.loadContinuousProfiler(...)` uses `ServiceLoader` to find `JavaContinuousProfilerProvider`
+- `AsyncProfilerContinuousProfilerProvider` instantiates `JavaContinuousProfiler`
+- `ProfilingServiceLoader.loadProfileConverter()` separately loads the `JavaProfileConverterProvider`
+
+### Profiling Flow
+
+**Start**
+- Sampling decision is made via `TracesSampler.sampleSessionProfile(...)`
+- Sampling is session-based and cached until `reevaluateSampling()`
+- Scopes and rate limiter are initialized lazily via `initScopes()`
+- Rate limits for `All` or `ProfileChunk` abort startup
+- JFR filename is generated under `profilingTracesDirPath`
+- async-profiler is started with a command like:
+ - `start,jfr,event=wall,nobatch,interval=,file=`
+- Automatic chunk stop is scheduled after `MAX_CHUNK_DURATION_MILLIS`
+
+**Chunk Rotation**
+- `stop(true)` stops async-profiler and validates the JFR file
+- A `ProfileChunk.Builder` is created with:
+ - current `profilerId`
+ - a fresh `chunkId`
+ - trace file
+ - chunk timestamp
+ - platform `java`
+- Builder is buffered in `payloadBuilders`
+- Chunks are sent if scopes are available
+- Profiling is restarted for the next chunk
+
+**Stop**
+- `MANUAL`: stop immediately, do not restart, reset `profilerId`
+- `TRACE`: decrement `rootSpanCounter`; stop only when it reaches 0
+- `close(...)` also forces shutdown and resets TRACE state
+
+### Sending and Conversion
+- `JavaContinuousProfiler` buffers `ProfileChunk.Builder` instances
+- `sendChunks(...)` builds `ProfileChunk` objects and calls `scopes.captureProfileChunk(...)`
+- `SentryClient.captureProfileChunk(...)` creates an envelope item
+- JVM JFR-to-`SentryProfile` conversion happens in `SentryEnvelopeItem.fromProfileChunk(...)` using the loaded `IProfileConverter`
+- Trace files are deleted in the envelope item path after serialization attempts
+
+## TRACE Mode Lifecycle
+- `rootSpanCounter` increments when sampled root spans start
+- `rootSpanCounter` decrements when root spans finish
+- Profiler runs while `rootSpanCounter > 0`
+- Multiple concurrent sampled transactions can share the same profiling session
+- Be careful when changing lifecycle logic: this area is lock-protected and concurrency-sensitive
+
+## Rate Limiting and Buffering
+
+### Rate Limiting
+- Registers as a `RateLimiter.IRateLimitObserver`
+- If rate limited for `ProfileChunk` or `All`:
+ - profiler stops immediately
+ - it does not auto-restart when the limit expires
+- Startup also checks rate limiting before profiling begins
+
+### Buffering / pre-init behavior
+- JFR files are written to `profilingTracesDirPath` and marked `deleteOnExit()` when a chunk is accepted
+- If scopes are not yet available, `ProfileChunk.Builder`s remain buffered in memory in `payloadBuilders`
+- This commonly matters for profiling that starts before SDK scopes are ready
+- This is not a dedicated durable offline queue owned by the profiler itself; conversion and final send happen later in the normal client/envelope path
+
+## Extending
+
+To add or replace JVM profiler implementations:
+- implement `IContinuousProfiler`
+- implement `JavaContinuousProfilerProvider`
+- register provider in:
+ - `META-INF/services/io.sentry.profiling.JavaContinuousProfilerProvider`
+
+To add or replace JVM profile conversion:
+- implement `IProfileConverter`
+- implement `JavaProfileConverterProvider`
+- register provider in:
+ - `META-INF/services/io.sentry.profiling.JavaProfileConverterProvider`
+
+## Code Locations
+
+Primary implementation:
+- `sentry/src/main/java/io/sentry/IContinuousProfiler.java`
+- `sentry/src/main/java/io/sentry/ProfileChunk.java`
+- `sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java`
+- `sentry/src/main/java/io/sentry/util/InitUtil.java`
+- `sentry/src/main/java/io/sentry/SentryEnvelopeItem.java`
+- `sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfiler.java`
+- `sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerContinuousProfilerProvider.java`
+- `sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerProfileConverterProvider.java`
+- `sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverter.java`
+
+Tests to read first:
+- `sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfilerTest.kt`
+- `sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/JavaContinuousProfilingServiceLoaderTest.kt`
+- `sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverterTest.kt`
+
+## LLM Guidance
+
+This rule is good enough for orientation, but for actual code changes always verify:
+- the sampling path in `TracesSampler`
+- continuous profiling enablement in `SentryOptions`
+- lifecycle entry points in `Scopes` and `SentryTracer`
+- conversion and file deletion behavior in `SentryEnvelopeItem`
+- existing tests before changing concurrency or lifecycle semantics
diff --git a/.cursor/rules/feature_flags.mdc b/.cursor/rules/feature_flags.mdc
new file mode 100644
index 00000000000..f2a78bc71cf
--- /dev/null
+++ b/.cursor/rules/feature_flags.mdc
@@ -0,0 +1,44 @@
+---
+alwaysApply: false
+description: Feature Flags
+---
+# Java SDK Feature Flags
+
+There is a scope based and a span based API for tracking feature flag evaluations.
+
+## Scope Based API
+
+The `addFeatureFlag` method can be used to track feature flag evaluations. It exists on `Sentry` static API as well as `IScopes` and `IScope`.
+
+When using static API, `IScopes` or COMBINED scope type, Sentry will also invoke `addFeatureFlag` on the current span. This does not happen, when directly invoking `addFeatureFlag` on `IScope` (except for COMBINED scope type).
+
+The `maxFeatureFlags` option controls how many flags are tracked per scope and also how many are sent to Sentry as part of events.
+Scope based feature flags can also be disabled by setting the value to 0. Defaults to 100 feature flag evaluations.
+
+Order of feature flag evaluations is important as we only keep track of the last {maxFeatureFlag} items.
+
+When a feature flag evaluation with the same name is added, the previous one is removed and the new one is stored so that it'll be dropped last.
+Refer to `FeatureFlagBuffer` fore more details. `FeatureFlagBuffer` has been optimized for storing scope based feature flag evaluations, especially clone performance.
+
+When sending out an error event, feature flag buffers from all three scope types (global, isolation and current scope) are merged, choosing the newest {maxFeatureFlag} entries across all scope types. Feature flags are sent as part of the `flags` context.
+
+## Span Based API
+
+It's also possible to use the `addFeatureFlag` method on `ISpan` (and by extension `ITransaction`). Feature flag evaluations tracked this way
+will not be added to the scope and thus won't be added to error events.
+
+Each span has its own `SpanFeatureFlagBuffer`. When starting a child span, feature flag evaluations are NOT copied from the parent. Each span starts out with an empty buffer and has its own limit.
+`SpanFeatureFlagBuffer` has been optimized for storing feature flag evaluations on spans.
+
+Spans have a hard coded limit of 10 feature flag evaluations. When full, new entries are rejected. Updates to existing entries are still allowed even if full.
+
+## Integrations
+
+We offer integrations that automatically track feature flag evaluations.
+
+Android:
+- LaunchDarkly (`SentryLaunchDarklyAndroidHook`)
+
+JVM (non Android):
+- LaunchDarkly (`SentryLaunchDarklyServerHook`)
+- OpenFeature (`SentryOpenFeatureHook`)
diff --git a/.cursor/rules/metrics.mdc b/.cursor/rules/metrics.mdc
new file mode 100644
index 00000000000..93c82ecb467
--- /dev/null
+++ b/.cursor/rules/metrics.mdc
@@ -0,0 +1,26 @@
+---
+alwaysApply: false
+description: Metrics API
+---
+# Java SDK Metrics API
+
+Metrics are enabled by default.
+
+API has been namespaced under `Sentry.metrics()` and `IScopes.metrics()` using the `IMetricsApi` interface and `MetricsApi` implementation.
+
+Options are namespaced under `SentryOptions.getMetrics()`.
+
+Three different APIs exist:
+- `count`: Counters are one of the more basic types of metrics and can be used to count certain event occurrences.
+- `distribution`: Distributions help you get the most insights from your data by allowing you to obtain aggregations such as p90, min, max, and avg.
+- `gauge`: Gauges let you obtain aggregates like min, max, avg, sum, and count. They can be represented in a more space-efficient way than distributions, but they can't be used to get percentiles. If percentiles aren't important to you, we recommend using gauges.
+
+Refer to `SentryMetricsEvent` for details about available fields.
+
+`MetricsBatchProcessor` handles batching (`MAX_BATCH_SIZE`), automatic sending of metrics after a timeout (`FLUSH_AFTER_MS`) and rejecting if `MAX_QUEUE_SIZE` has been hit.
+
+The flow is `IMetricsApi` -> `IMetricsBatchProcessor` -> `SentryClient.captureBatchedMetricsEvents` -> `ITransport`.
+
+Each `SentryMetricsEvent` goes through `SentryOptions.metrics.beforeSend` (if configured) and can be modified or dropped.
+
+For sending, a batch of `SentryMetricsEvent` objects is sent inside a `SentryMetricsEvents` object.
diff --git a/.cursor/rules/opentelemetry.mdc b/.cursor/rules/opentelemetry.mdc
index 7a94dcf58f4..4e773233f04 100644
--- a/.cursor/rules/opentelemetry.mdc
+++ b/.cursor/rules/opentelemetry.mdc
@@ -14,6 +14,8 @@ The Sentry Java SDK provides comprehensive OpenTelemetry integration through mul
- `sentry-opentelemetry-agentless-spring`: Spring-specific agentless integration
- `sentry-opentelemetry-bootstrap`: Classes that go into the bootstrap classloader when the agent is used. For agentless they are simply used in the applications classloader.
- `sentry-opentelemetry-agentcustomization`: Classes that help wire up Sentry in OpenTelemetry. These land in the agent classloader when the agent is used. For agentless they are simply used in the application classloader.
+- `sentry-opentelemetry-otlp`: Classes for using OpenTelemetry to send spans to Sentry using the OTLP endpoint and have Sentry use OpenTelemetry trace and span id.
+- `sentry-opentelemetry-otlp-spring`: Spring Boot convenience module that includes `sentry-opentelemetry-otlp` and the OpenTelemetry Spring Boot starter as transitive dependencies.
## Advantages over using Sentry without OpenTelemetry
@@ -86,3 +88,10 @@ After creating the transaction with child spans `SentrySpanExporter` uses Sentry
## Troubleshooting
To debug forking of `Scopes`, we added a reference to `parent` `Scopes` and a `creator` String to store the reason why `Scopes` were created or forked.
+
+# OTLP
+When using `sentry-opentelemetry-otlp`, Sentry only loads trace ID and span ID from OpenTelemetry `Context` (via `OpenTelemetryOtlpEventProcessor`). Sentry does not rely on OpenTelemetry `Context` for scope storage and propagation, instead relying on its `DefaultScopesStorage`.
+It is common to keep Performance in Sentry SDK disabled since that part is taken over by OpenTelemetry.
+The `sentry-opentelemetry-otlp` module is not connected to the other `sentry-opentelemetry-*` modules but instead intended only when the goal is to run OpenTelemetry for creating spans and Sentry for other products like errors, logs, metrics etc.
+The `sentry-opentelemetry-otlp-spring` module wraps `sentry-opentelemetry-otlp` and includes the OpenTelemetry Spring Boot starter for easier setup in Spring Boot applications.
+The OTLP module does not easily work with the OpenTelemetry agent as it would require customizing the agent.JAR in order to get the propagator loaded.
diff --git a/.cursor/rules/options.mdc b/.cursor/rules/options.mdc
new file mode 100644
index 00000000000..2d239da7813
--- /dev/null
+++ b/.cursor/rules/options.mdc
@@ -0,0 +1,115 @@
+---
+alwaysApply: false
+description: Adding and modifying SDK options
+---
+# Adding Options to the SDK
+
+New features must be **opt-in by default**. Options control whether a feature is enabled and how it behaves.
+
+## Namespaced Options
+
+Newer features use namespaced option classes nested inside `SentryOptions`, e.g.:
+- `SentryOptions.getLogs()` → `SentryOptions.Logs`
+- `SentryOptions.getMetrics()` → `SentryOptions.Metrics`
+
+Each namespaced options class is a `public static final class` inside `SentryOptions` with its own fields, getters/setters, and callbacks (e.g. `BeforeSendLogCallback`, `BeforeSendMetricCallback`).
+
+A typical namespaced options class contains:
+- `enabled` boolean (default `false` for opt-in)
+- `sampleRate` double (if the feature supports sampling)
+- `beforeSend` callback interface (nested inside the options class)
+
+To add a new namespaced options class:
+1. Create the `public static final class` inside `SentryOptions` with fields, getters/setters, and any callback interfaces
+2. Add a private field on `SentryOptions` initialized with `new SentryOptions.MyFeature()`
+3. Add getter/setter on `SentryOptions` annotated with `@ApiStatus.Experimental`
+
+## Direct (Non-Namespaced) Options
+
+Options that apply globally across the SDK (e.g. `dsn`, `environment`, `release`, `sampleRate`, `maxBreadcrumbs`) live as direct fields on `SentryOptions` with getter/setter pairs. Use this pattern for options that aren't tied to a specific feature namespace.
+
+## Configuration Layers
+
+Options can be set through multiple layers. When adding a new option, consider which layers apply:
+
+### 1. SentryOptions (always required)
+
+The core options class. Add the field (or nested class) with getter/setter here.
+
+**File:** `sentry/src/main/java/io/sentry/SentryOptions.java`
+
+**Tests:** `sentry/src/test/java/io/sentry/SentryOptionsTest.kt`
+- Test the default value
+- Test merge behavior (see layer 2)
+
+### 2. ExternalOptions (sentry.properties / environment variables)
+
+Allows setting options via `sentry.properties` file or system properties. Fields use nullable wrapper types (`@Nullable Boolean`, `@Nullable Double`) since unset means "don't override the default."
+
+**File:** `sentry/src/main/java/io/sentry/ExternalOptions.java`
+- Add `@Nullable` fields with getter/setter for each externally configurable option (e.g. `enableMetrics`, `logsSampleRate`)
+- Wire them in the static `from(PropertiesProvider)` method:
+ - Boolean: `propertiesProvider.getBooleanProperty("metrics.enabled")`
+ - Double: `propertiesProvider.getDoubleProperty("logs.sample-rate")`
+
+**File:** `sentry/src/main/java/io/sentry/SentryOptions.java` — `merge()` method
+- Add null-check blocks to apply each external option onto the namespaced options class:
+ ```java
+ if (options.isEnableMetrics() != null) {
+ getMetrics().setEnabled(options.isEnableMetrics());
+ }
+ if (options.getLogsSampleRate() != null) {
+ getLogs().setSampleRate(options.getLogsSampleRate());
+ }
+ ```
+
+**Tests:**
+- `sentry/src/test/java/io/sentry/ExternalOptionsTest.kt` — test true/false/null for booleans, valid values and null for doubles
+- `sentry/src/test/java/io/sentry/SentryOptionsTest.kt` — test merge applies values and test merge preserves defaults when unset
+
+### 3. Android Manifest Metadata (Android only)
+
+Allows setting options via `AndroidManifest.xml` `` tags.
+
+**File:** `sentry-android-core/src/main/java/io/sentry/android/core/ManifestMetadataReader.java`
+- Add a `static final String` constant for the key (e.g. `"io.sentry.metrics.enabled"`)
+- Read it in `applyMetadata()` using `readBool(metadata, logger, CONSTANT, defaultValue)`
+- Apply to the namespaced options, e.g. `options.getMetrics().setEnabled(...)`
+
+**Tests:** `sentry-android-core/src/test/java/io/sentry/android/core/ManifestMetadataReaderTest.kt`
+- Test default value preserved when not in manifest
+- Test explicit true
+- Test explicit false
+
+### 4. Spring Boot Properties (Spring Boot only)
+
+`SentryProperties` extends `SentryOptions`, so namespaced options (nested classes) are automatically available as Spring Boot properties without extra code. For example, `SentryOptions.Logs` is automatically mapped to `sentry.logs.enabled` in `application.properties`.
+
+No additional code is needed for namespaced options — Spring Boot auto-configuration handles this via property binding on the `SentryOptions` class hierarchy.
+
+**Tests:** `sentry-spring-boot*/src/test/kotlin/.../SentryAutoConfigurationTest.kt`
+- Add the property (e.g. `"sentry.logs.enabled=true"`) to the existing `resolves all properties` test
+- Assert the value is set on the resolved `SentryProperties` bean
+- There are three Spring Boot modules with separate test files: `sentry-spring-boot`, `sentry-spring-boot-jakarta`, `sentry-spring-boot-4`
+
+### 5. Reading Options at Runtime
+
+Features check their options at usage time. For namespaced features the check typically happens in the feature's API class (e.g. `LoggerApi`, `MetricsApi`):
+- Check `options.getLogs().isEnabled()` early and return if disabled
+- Apply sampling via `options.getLogs().getSampleRate()` if applicable
+- Apply `beforeSend` callback in `SentryClient` before sending
+
+When a feature has its own capture path (e.g. `captureLog`), the relevant classes are:
+- `ISentryClient` — add the capture method signature
+- `SentryClient` — implement capture, including `beforeSend` callback execution
+- `NoOpSentryClient` — add no-op stub
+
+## Checklist for Adding a New Namespaced Option
+
+1. `SentryOptions.java` — nested options class + getter/setter on `SentryOptions`
+2. `ExternalOptions.java` — `@Nullable` fields + wiring in `from()`
+3. `SentryOptions.java` `merge()` — apply external options to namespaced class
+4. `ManifestMetadataReader.java` — Android manifest support (if Android-relevant)
+5. `SentryAutoConfigurationTest.kt` — Spring Boot property binding tests (all three Spring Boot modules)
+6. Tests for all of the above (`SentryOptionsTest`, `ExternalOptionsTest`, `ManifestMetadataReaderTest`)
+7. Run `./gradlew apiDump` — the nested class and its methods appear in `sentry.api`
diff --git a/.cursor/rules/overview_dev.mdc b/.cursor/rules/overview_dev.mdc
deleted file mode 100644
index 89c70e2c158..00000000000
--- a/.cursor/rules/overview_dev.mdc
+++ /dev/null
@@ -1,65 +0,0 @@
----
-alwaysApply: true
-description: Sentry Java SDK - Development Rules Overview
----
-
-# Sentry Java SDK Development Rules
-
-## Always Applied Rules
-
-These rules are automatically included in every conversation:
-- **coding.mdc**: General contributing guidelines, build commands, and workflow rules
-
-## Domain-Specific Rules (Fetch Only When Needed)
-
-Use the `fetch_rules` tool to include these rules when working on specific areas:
-
-### Core SDK Functionality
-- **`scopes`**: Use when working with:
- - Hub/Scope management, forking, or lifecycle
- - `Sentry.getCurrentScopes()`, `pushScope()`, `withScope()`
- - `ScopeType` (GLOBAL, ISOLATION, CURRENT)
- - Thread-local storage, scope bleeding issues
- - Migration from Hub API (v7 → v8)
-
-- **`deduplication`**: Use when working with:
- - Duplicate event detection/prevention
- - `DuplicateEventDetectionEventProcessor`
- - `enableDeduplication` option
-
-- **`offline`**: Use when working with:
- - Caching, envelope storage/retrieval
- - Network failure handling, retry logic
- - `AsyncHttpTransport`, `EnvelopeCache`
- - Rate limiting, cache rotation
- - Android vs JVM caching differences
-
-### Integration & Infrastructure
-- **`opentelemetry`**: Use when working with:
- - OpenTelemetry modules (`sentry-opentelemetry-*`)
- - Agent vs agentless configurations
- - Span processing, sampling, context propagation
- - `OtelSpanFactory`, `SentrySpanExporter`
- - Tracing, distributed tracing
-
-- **`new_module`**: Use when adding a new integration or sample module
-
-### Testing
-- **`e2e_tests`**: Use when working with:
- - System tests, sample applications
- - `system-test-runner.py`, mock Sentry server
- - End-to-end test infrastructure
- - CI system test workflows
-
-## Usage Guidelines
-
-1. **Start minimal**: Only include `coding.mdc` (auto-applied) for general tasks
-2. **Fetch on-demand**: Use `fetch_rules ["rule_name"]` when you identify specific domain work
-3. **Multiple rules**: Fetch multiple rules if task spans domains (e.g., `["scopes", "opentelemetry"]` for tracing scope issues)
-4. **Context clues**: Look for these keywords in requests to determine relevant rules:
- - Scope/Hub/forking → `scopes`
- - Duplicate/dedup → `deduplication`
- - OpenTelemetry/tracing/spans → `opentelemetry`
- - new module/integration/sample → `new_module`
- - Cache/offline/network → `offline`
- - System test/e2e/sample → `e2e_tests`
diff --git a/.cursor/rules/queues.mdc b/.cursor/rules/queues.mdc
new file mode 100644
index 00000000000..fe082c3b854
--- /dev/null
+++ b/.cursor/rules/queues.mdc
@@ -0,0 +1,82 @@
+---
+alwaysApply: false
+description: Sentry Queues module and Java SDK queue tracing
+---
+# Sentry Queues and Java SDK Queue Tracing
+
+## Product model
+
+Sentry Queues is built from tracing data. SDKs mark queue work with queue-specific span operations and messaging span data so Sentry can identify producers, consumers, destinations, latency, and failures.
+
+The important concepts are:
+- `queue.publish`: a span for enqueueing/publishing a message to a queue or topic.
+- `queue.process`: a transaction for processing a dequeued message.
+- Messaging span data, especially:
+ - `messaging.system` (for example `kafka`)
+ - `messaging.destination.name` (queue/topic name)
+ - `messaging.message.id`
+ - `messaging.message.retry.count`
+ - `messaging.message.body.size`
+ - `messaging.message.envelope.size`
+ - `messaging.message.receive.latency`
+- Distributed tracing headers (`sentry-trace` and `baggage`) link producer-side work to consumer-side processing.
+- Queue receive latency is the time a message spent waiting between publish/enqueue and processing. For Java Kafka, this comes from the `sentry-task-enqueued-time` header that the producer writes and the consumer reads.
+
+The Queues UI is not backed by a separate Java event type. The Java SDK contributes data through spans/transactions with the expected operations, trace context, statuses, and messaging attributes.
+
+## Java SDK implementation
+
+Queue tracing is opt-in. `SentryOptions.isEnableQueueTracing()` defaults to `false` and can be enabled with `setEnableQueueTracing(true)` or external config key `enable-queue-tracing` (`sentry.enable-queue-tracing` in Spring Boot). Captured queue spans/transactions still depend on tracing being enabled and sampled.
+
+Kafka support lives in `sentry-kafka`:
+- `SentryKafkaProducer.wrap(Producer)` wraps Kafka `Producer.send(...)` calls.
+ - Creates a `queue.publish` child span when there is an active span.
+ - Sets `messaging.system=kafka` and `messaging.destination.name=`.
+ - Injects `sentry-trace`, `baggage`, and `sentry-task-enqueued-time` headers.
+ - Still injects tracing/enqueued-time headers when queue tracing is enabled but there is no active span, so background producers can link to consumers.
+ - Finishes the span from the Kafka callback with `OK` or `INTERNAL_ERROR`.
+- `SentryKafkaConsumerTracing.withTracing(record, callback)` is the manual raw-Kafka consumer helper.
+ - Forks root scopes for the processing lifecycle and makes them current.
+ - Continues the trace from Kafka headers.
+ - Starts a `queue.process` transaction bound to scope when tracing is enabled.
+ - Sets Kafka messaging data, body size, retry count, and receive latency when available.
+ - Finishes with `OK` or `INTERNAL_ERROR` and never lets instrumentation failures break customer processing.
+
+Spring Kafka support lives in `sentry-spring`, `sentry-spring-jakarta`, and `sentry-spring-7`:
+- `SentryKafkaProducerBeanPostProcessor` installs a producer post-processor on `DefaultKafkaProducerFactory` and wraps created producers with `SentryKafkaProducer.wrap(...)`.
+- `SentryKafkaConsumerBeanPostProcessor` installs `SentryKafkaRecordInterceptor` on listener container factories.
+- `SentryKafkaRecordInterceptor` starts/finishes `queue.process` transactions around listener processing, continues traces from headers, forks scopes for the record lifecycle, and preserves any existing delegate interceptor.
+- Spring Boot auto-configuration registers both post-processors only when Spring Kafka and `sentry-kafka` are present and `sentry.enable-queue-tracing=true`.
+- Spring Boot queue auto-configuration is disabled when Sentry OpenTelemetry integration classes are present to avoid duplicate Kafka instrumentation.
+
+## Trace origins and suppression
+
+Queue instrumentation sets span origins so it can be identified and suppressed with `ignoredSpanOrigins`:
+- Raw Kafka producer: `auto.queue.kafka.producer`
+- Raw Kafka consumer helper: `manual.queue.kafka.consumer`
+- Spring Kafka producer: `auto.queue.spring.kafka.producer`, `auto.queue.spring_jakarta.kafka.producer`, `auto.queue.spring7.kafka.producer`
+- Spring Kafka consumer: `auto.queue.spring.kafka.consumer`, `auto.queue.spring_jakarta.kafka.consumer`, `auto.queue.spring7.kafka.consumer`
+
+## Files to inspect when changing queue tracing
+
+- Core option and conventions:
+ - `sentry/src/main/java/io/sentry/SentryOptions.java`
+ - `sentry/src/main/java/io/sentry/ExternalOptions.java`
+ - `sentry/src/main/java/io/sentry/SpanDataConvention.java`
+- Raw Kafka:
+ - `sentry-kafka/src/main/java/io/sentry/kafka/SentryKafkaProducer.java`
+ - `sentry-kafka/src/main/java/io/sentry/kafka/SentryKafkaConsumerTracing.java`
+ - `sentry-kafka/src/test/kotlin/io/sentry/kafka/*Test.kt`
+- Spring Kafka:
+ - `sentry-spring*/src/main/java/io/sentry/**/kafka/*`
+ - `sentry-spring*/src/test/kotlin/io/sentry/**/kafka/*Test.kt`
+ - `sentry-spring-boot*/src/main/java/io/sentry/**/SentryAutoConfiguration.java`
+ - `sentry-spring-boot*/src/test/kotlin/io/sentry/**/SentryKafkaAutoConfigurationTest.kt`
+
+## Related rules
+
+Also fetch:
+- `options` when changing `enableQueueTracing` or configuration surfaces.
+- `scopes` when changing consumer scope forking/lifecycle.
+- `opentelemetry` when changing coexistence with OTel auto-instrumentation.
+- `api` when changing public Kafka APIs or option methods.
diff --git a/.cursor/rules/scopes.mdc b/.cursor/rules/scopes.mdc
index 179b0943565..e054755d4f5 100644
--- a/.cursor/rules/scopes.mdc
+++ b/.cursor/rules/scopes.mdc
@@ -49,6 +49,15 @@ Data is also passed on to newly forked child scopes but not to parents.
Current scope can be retrieved from `Scopes` via `getScope`.
+### Combined Scope
+
+This is a special scope type that combines global, isolation and current scope.
+
+Refer to `CombinedScopeView` for each field of interest to see whether values from the three individual scopes are merged,
+whether a specific one is used or whether we're simply using the first one that has a value.
+
+Also see the section about `defaultScopeType` further down.
+
## Storage of `Scopes`
`Scopes` are stored in a `ThreadLocal` by default (NOTE: this is different for OpenTelemetry, see opentelemetry.mdc).
diff --git a/.envrc b/.envrc
index 97b3f16c6f7..f58a7cee600 100644
--- a/.envrc
+++ b/.envrc
@@ -1,2 +1,3 @@
-export VIRTUAL_ENV=".venv"
-layout python3
+export VIRTUAL_ENV="${PWD}/.venv"
+devenv sync
+PATH_add "${PWD}/.venv/bin"
diff --git a/.gitattributes b/.gitattributes
index d952371fb7e..f444fd5957d 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,6 +1,12 @@
* text eol=lf
*.png binary
*.jpg binary
+*.pb binary
+*.gz binary
+*.bin binary
+*.zip binary
+*.jar binary
+*.gpg binary
# These are explicitly windows files and should use crlf
*.bat text eol=crlf
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index a19e12c1c1b..6e1f71a7677 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -1 +1 @@
-* @adinauer @romtsn @stefanosiano @markushi @lcian
+* @adinauer @romtsn @markushi @runningcode @0xadam-brown
diff --git a/.github/ISSUE_TEMPLATE/bug_report_android.yml b/.github/ISSUE_TEMPLATE/bug_report_android.yml
index f76c38dbe75..5dff43579c6 100644
--- a/.github/ISSUE_TEMPLATE/bug_report_android.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report_android.yml
@@ -16,6 +16,7 @@ body:
- sentry-apollo
- sentry-apollo-3
- sentry-compose
+ - sentry-launchdarkly-android
- sentry-okhttp
- other
validations:
@@ -54,6 +55,22 @@ body:
validations:
required: true
+ - type: dropdown
+ id: other_error_monitoring_solution
+ attributes:
+ description: Are you using any other error monitoring solution alongside Sentry?
+ label: Other Error Monitoring Solution
+ options:
+ - "No"
+ - "Bugsnag"
+ - "Datadog"
+ - "Firebase Crashlytics"
+ - "Instabug/Luciq"
+ - "NewRelic"
+ - "Other (please mention in issue description)"
+ validations:
+ required: true
+
- type: input
id: version
attributes:
diff --git a/.github/ISSUE_TEMPLATE/bug_report_java.yml b/.github/ISSUE_TEMPLATE/bug_report_java.yml
index a7ca3cbb770..8355d75a43b 100644
--- a/.github/ISSUE_TEMPLATE/bug_report_java.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report_java.yml
@@ -35,6 +35,8 @@ body:
- sentry-graphql-22
- sentry-quartz
- sentry-openfeign
+ - sentry-openfeature
+ - sentry-launchdarkly-server
- sentry-apache-http-client-5
- sentry-okhttp
- sentry-reactor
@@ -51,6 +53,25 @@ body:
validations:
required: true
+ - type: dropdown
+ id: other_error_monitoring
+ attributes:
+ description: Are you using any other error monitoring solution alongside Sentry?
+ label: Other Error Monitoring Solution
+ options:
+ - "Yes"
+ - "No"
+ validations:
+ required: true
+
+ - type: input
+ id: other_error_monitoring_name
+ attributes:
+ label: Other Error Monitoring Solution Name
+ description: If you're using another error monitoring solution side-by-side, please enter the name of the other solution.
+ validations:
+ required: false
+
- type: input
id: version
attributes:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index b88a67a7f0c..2824699563c 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,6 +1,28 @@
version: 2
+registries:
+ gradle-plugin-portal:
+ type: maven-repository
+ url: https://plugins.gradle.org/m2
+ username: dummy # Required by dependabot
+ password: dummy # Required by dependabot
updates:
+ - package-ecosystem: "gradle"
+ directory: "/"
+ registries:
+ - gradle-plugin-portal
+ schedule:
+ interval: "daily"
+ ignore:
+ - dependency-name: "org.springframework.boot*"
+ commit-message:
+ prefix: "chore(deps)"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
- interval: weekly
+ interval: "daily"
+ commit-message:
+ prefix: "chore(deps)"
+ groups:
+ github-actions:
+ patterns:
+ - "*"
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index b337ac9ea4e..baa2dad44a2 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,5 +1,5 @@
## :scroll: Description
-
+
## :bulb: Motivation and Context
@@ -12,6 +12,10 @@
-->
## :green_heart: How did you test it?
+
## :pencil: Checklist
@@ -25,6 +29,7 @@
- [ ] Review from the native team if needed.
- [ ] No breaking change or entry added to the changelog.
- [ ] No breaking change for hybrid SDKs or communicated to hybrid SDKs.
+- [ ] Public API changes reviewed by another Mobile SDK team member or implemented according to the [develop docs](https://develop.sentry.dev/) spec.
## :crystal_ball: Next steps
diff --git a/.github/workflows/agp-matrix.yml b/.github/workflows/agp-matrix.yml
index da4b5cf0837..4dc779ce812 100644
--- a/.github/workflows/agp-matrix.yml
+++ b/.github/workflows/agp-matrix.yml
@@ -17,7 +17,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- agp: [ '8.7.0','8.8.0','8.9.0' ]
+ agp: [ '9.0.0', '9.1.1', '9.2.1' ]
integrations: [ true, false ]
name: AGP Matrix Release - AGP ${{ matrix.agp }} - Integrations ${{ matrix.integrations }}
@@ -28,18 +28,18 @@ jobs:
steps:
- name: Checkout Repo
- uses: actions/checkout@v5
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
submodules: 'recursive'
- name: Setup Java Version
- uses: actions/setup-java@v5
+ uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a
+ uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
@@ -50,7 +50,7 @@ jobs:
sudo udevadm trigger --name-match=kvm
- name: AVD cache
- uses: actions/cache@v4
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
id: avd-cache
with:
path: |
@@ -60,7 +60,7 @@ jobs:
- name: Create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
- uses: reactivecircus/android-emulator-runner@1dcd0090116d15e7c562f8db72807de5e036a4ed # pin@v2
+ uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # pin@v2
with:
api-level: 30
target: aosp_atd
@@ -79,7 +79,7 @@ jobs:
# We tried to use the cache action to cache gradle stuff, but it made tests slower and timeout
- name: Run instrumentation tests
- uses: reactivecircus/android-emulator-runner@1dcd0090116d15e7c562f8db72807de5e036a4ed # pin@v2
+ uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # pin@v2
with:
api-level: 30
target: aosp_atd
@@ -90,11 +90,11 @@ jobs:
disable-spellchecker: true
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
disk-size: 4096M
- script: ./gradlew sentry-android-integration-tests:sentry-uitest-android:connectedReleaseAndroidTest -DtestBuildType=release -Denvironment=github --daemon
+ script: ./gradlew sentry-android-integration-tests:sentry-uitest-android:connectedReleaseAndroidTest -Denvironment=github --daemon
- name: Upload test results
if: always()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: test-results-AGP${{ matrix.agp }}-Integrations${{ matrix.integrations }}
path: |
@@ -103,7 +103,7 @@ jobs:
**/build/outputs/mapping/release/*
- name: Test Report
- uses: phoenix-actions/test-reporting@f957cd93fc2d848d556fa0d03c57bc79127b6b5e # pin@v15
+ uses: phoenix-actions/test-reporting@7317eea6e13c47348dd0bb318669485157c518d6 # pin@v16
if: always()
with:
name: JUnit AGP ${{ matrix.agp }} - Integrations ${{ matrix.integrations }}
@@ -112,10 +112,3 @@ jobs:
reporter: java-junit
output-to: step-summary
fail-on-error: false
-
- - name: Upload test results to Codecov
- if: ${{ !cancelled() }}
- uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f
- with:
- token: ${{ secrets.CODECOV_TOKEN }}
- file: build/outputs/androidTest-results/**/*.xml
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 028cd65f424..ef9aa7cfc36 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -19,48 +19,56 @@ jobs:
steps:
- name: Checkout Repo
- uses: actions/checkout@v5
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
submodules: 'recursive'
- name: Setup Java Version
- uses: actions/setup-java@v5
+ uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
with:
distribution: 'temurin'
java-version: '17'
# Workaround for https://github.com/gradle/actions/issues/21 to use config cache
- name: Cache buildSrc
- uses: actions/cache@v4
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: buildSrc/build
key: build-logic-${{ hashFiles('buildSrc/src/**', 'buildSrc/build.gradle.kts','buildSrc/settings.gradle.kts') }}
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a
+ uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
- - name: Run Tests with coverage and Lint
+ - name: Run Tests and Lint
run: make preMerge
- - name: Upload coverage to Codecov
- uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # pin@v4
- with:
- name: sentry-java
- fail_ci_if_error: false
- token: ${{ secrets.CODECOV_TOKEN }}
+ - name: Install Sentry CLI
+ uses: getsentry/action-setup-cli@70d7e587b84c2e78cf4d37cd33d7b74fb3729c1b # v1
+
+ - name: Upload Snapshots to Sentry
+ # Skip on PRs from forks, which don't have access to the upload secret
+ if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
+ run: |
+ sentry-cli snapshots upload ./sentry-android-core/build/test-snapshots \
+ --app-id sentry-android-core
+ env:
+ SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
+ SENTRY_ORG: sentry-sdks
+ SENTRY_PROJECT: sentry-android
- name: Upload test results
if: always()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: test-results-build
path: |
**/build/reports/*
- name: Test Report
- uses: phoenix-actions/test-reporting@f957cd93fc2d848d556fa0d03c57bc79127b6b5e # pin@v15
+ uses: phoenix-actions/test-reporting@7317eea6e13c47348dd0bb318669485157c518d6 # pin@v16
if: always()
with:
name: JUnit Build
diff --git a/.github/workflows/changes-in-high-risk-code.yml b/.github/workflows/changes-in-high-risk-code.yml
index e9c436ea253..44d65924209 100644
--- a/.github/workflows/changes-in-high-risk-code.yml
+++ b/.github/workflows/changes-in-high-risk-code.yml
@@ -16,10 +16,10 @@ jobs:
high_risk_code: ${{ steps.changes.outputs.high_risk_code }}
high_risk_code_files: ${{ steps.changes.outputs.high_risk_code_files }}
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Get changed files
id: changes
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
+ uses: dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4.0.2
with:
token: ${{ github.token }}
filters: .github/file-filters.yml
@@ -34,7 +34,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Comment on PR to notify of changes in high risk files
- uses: actions/github-script@v8
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
high_risk_code: ${{ needs.files-changed.outputs.high_risk_code_files }}
with:
diff --git a/.github/workflows/check-tombstone-proto-schema.yml b/.github/workflows/check-tombstone-proto-schema.yml
new file mode 100644
index 00000000000..0190865250e
--- /dev/null
+++ b/.github/workflows/check-tombstone-proto-schema.yml
@@ -0,0 +1,16 @@
+name: Check Tombstone Proto Schema
+
+on:
+ schedule:
+ - cron: '0 9 * * *'
+ workflow_dispatch:
+
+jobs:
+ check:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+
+ - name: Check for newer Tombstone proto schema
+ run: ./scripts/check-tombstone-proto-schema.sh
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 5fa5aea6989..57e2e4a1073 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -20,23 +20,23 @@ jobs:
steps:
- name: Checkout Repo
- uses: actions/checkout@v5
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
submodules: 'recursive'
- name: Setup Java Version
- uses: actions/setup-java@v5
+ uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a
+ uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
- name: Initialize CodeQL
- uses: github/codeql-action/init@192325c86100d080feab897ff886c34abd4c83a3 # pin@v2
+ uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # pin@v2
with:
languages: 'java'
@@ -45,4 +45,4 @@ jobs:
./gradlew buildForCodeQL --no-build-cache
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@192325c86100d080feab897ff886c34abd4c83a3 # pin@v2
+ uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # pin@v2
diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml
index 000b75ff3ee..e40b4563b00 100644
--- a/.github/workflows/danger.yml
+++ b/.github/workflows/danger.yml
@@ -2,8 +2,10 @@ name: Danger
on:
pull_request:
- types: [opened, synchronize, reopened, edited, ready_for_review]
+ types: [opened, synchronize, reopened, edited, ready_for_review, labeled, unlabeled]
jobs:
danger:
- uses: getsentry/github-workflows/.github/workflows/danger.yml@v2
+ runs-on: ubuntu-latest
+ steps:
+ - uses: getsentry/github-workflows/danger@607fed74f812e69201531a5185b6c3c57caa4e89 # v3
diff --git a/.github/workflows/enforce-license-compliance.yml b/.github/workflows/enforce-license-compliance.yml
index 9c31d4dba9f..a7be2bdb001 100644
--- a/.github/workflows/enforce-license-compliance.yml
+++ b/.github/workflows/enforce-license-compliance.yml
@@ -11,23 +11,23 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a
+ uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
- name: Set up Java
- uses: actions/setup-java@v5
+ uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
with:
distribution: 'temurin'
java-version: '17'
- name: Checkout
- uses: actions/checkout@v5
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# TODO: remove this when upstream is fixed
- name: Disable Gradle configuration cache (see https://github.com/fossas/fossa-cli/issues/872)
run: sed -i 's/^org.gradle.configuration-cache=.*/org.gradle.configuration-cache=false/' gradle.properties
- name: 'Enforce License Compliance'
- uses: getsentry/action-enforce-license-compliance@main
+ uses: getsentry/action-enforce-license-compliance@48236a773346cb6552a7bda1ee370d2797365d87 # main
with:
skip_checkout: 'true'
fossa_test_timeout_seconds: 3600
diff --git a/.github/workflows/format-code.yml b/.github/workflows/format-code.yml
index 64f022a4b1b..7f638963fc0 100644
--- a/.github/workflows/format-code.yml
+++ b/.github/workflows/format-code.yml
@@ -8,18 +8,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@v5
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
submodules: 'recursive'
- name: set up JDK 17
- uses: actions/setup-java@v5
+ uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a
+ uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
diff --git a/.github/workflows/generate-javadocs.yml b/.github/workflows/generate-javadocs.yml
index f94dffe3778..ad33bb93e7a 100644
--- a/.github/workflows/generate-javadocs.yml
+++ b/.github/workflows/generate-javadocs.yml
@@ -9,24 +9,24 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
- uses: actions/checkout@v5
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
submodules: 'recursive'
- name: set up JDK 17
- uses: actions/setup-java@v5
+ uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a
+ uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
- name: Generate Aggregate Javadocs
run: |
./gradlew aggregateJavadocs
- name: Deploy
- uses: JamesIves/github-pages-deploy-action@6c2d9db40f9296374acc17b90404b6e8864128c8 # pin@4.7.3
+ uses: JamesIves/github-pages-deploy-action@d92aa235d04922e8f08b40ce78cc5442fcfbfa2f # pin@4.8.0
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: gh-pages
diff --git a/.github/workflows/integration-tests-benchmarks.yml b/.github/workflows/integration-tests-benchmarks.yml
index c005b6a6286..66e4498dcb5 100644
--- a/.github/workflows/integration-tests-benchmarks.yml
+++ b/.github/workflows/integration-tests-benchmarks.yml
@@ -27,18 +27,18 @@ jobs:
steps:
- name: Git checkout
- uses: actions/checkout@v5
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
submodules: 'recursive'
- name: 'Set up Java: 17'
- uses: actions/setup-java@v5
+ uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a
+ uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
@@ -48,7 +48,7 @@ jobs:
run: make assembleBenchmarks
- name: Run All Tests in SauceLab
- uses: saucelabs/saucectl-run-action@39e4f0666ca8ecb4b60847213c6e0fbd6a0c2bd8 # pin@v3
+ uses: saucelabs/saucectl-run-action@283660aa934c02723c497efa151d582a3acc5801 # pin@v3
if: github.event_name != 'pull_request' && env.SAUCE_USERNAME != null
env:
GITHUB_TOKEN: ${{ github.token }}
@@ -58,7 +58,7 @@ jobs:
config-file: .sauce/sentry-uitest-android-benchmark.yml
- name: Run one test in SauceLab
- uses: saucelabs/saucectl-run-action@39e4f0666ca8ecb4b60847213c6e0fbd6a0c2bd8 # pin@v3
+ uses: saucelabs/saucectl-run-action@283660aa934c02723c497efa151d582a3acc5801 # pin@v3
if: github.event_name == 'pull_request' && env.SAUCE_USERNAME != null
env:
GITHUB_TOKEN: ${{ github.token }}
@@ -77,22 +77,22 @@ jobs:
steps:
- name: Git checkout
- uses: actions/checkout@v5
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
submodules: 'recursive'
- name: 'Set up Java: 17'
- uses: actions/setup-java@v5
+ uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a
+ uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
- - uses: actions/cache@v4
+ - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
id: app-plain-cache
with:
path: sentry-android-integration-tests/test-app-plain/build/outputs/apk/release/test-app-plain-release.apk
@@ -106,7 +106,7 @@ jobs:
run: ./gradlew :sentry-android-integration-tests:test-app-sentry:assembleRelease
- name: Collect app metrics
- uses: getsentry/action-app-sdk-overhead-metrics@v1
+ uses: getsentry/action-app-sdk-overhead-metrics@44fb5489ac4ac252c87d84811972dc93a1e490b8
with:
config: sentry-android-integration-tests/metrics-test.yml
sauce-user: ${{ secrets.SAUCE_USERNAME }}
diff --git a/.github/workflows/integration-tests-size.yml b/.github/workflows/integration-tests-size.yml
new file mode 100644
index 00000000000..3e76c951f86
--- /dev/null
+++ b/.github/workflows/integration-tests-size.yml
@@ -0,0 +1,46 @@
+name: SDK Size Analysis
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ build:
+ name: Build and Analyze SDK Size
+ runs-on: ubuntu-latest
+
+ env:
+ GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
+
+ steps:
+ - name: Checkout Repo
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+
+ - name: Setup Java Version
+ uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
+ with:
+ distribution: "temurin"
+ java-version: "17"
+
+ # Workaround for https://github.com/gradle/actions/issues/21 to use config cache
+ - name: Cache buildSrc
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ with:
+ path: buildSrc/build
+ key: build-logic-${{ hashFiles('buildSrc/src/**', 'buildSrc/build.gradle.kts','buildSrc/settings.gradle.kts') }}
+
+ - name: Setup Gradle
+ uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
+ with:
+ cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
+
+ - name: Size Analysis
+ run: ./gradlew :sentry-android-integration-tests:test-app-size:bundleRelease
+ env:
+ SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
diff --git a/.github/workflows/integration-tests-ui-critical.yml b/.github/workflows/integration-tests-ui-critical.yml
index fb01f40f82d..dd99f8c6b7c 100644
--- a/.github/workflows/integration-tests-ui-critical.yml
+++ b/.github/workflows/integration-tests-ui-critical.yml
@@ -15,7 +15,7 @@ env:
BUILD_PATH: "build/outputs/apk/release"
APK_NAME: "sentry-uitest-android-critical-release.apk"
APK_ARTIFACT_NAME: "sentry-uitest-android-critical-release"
- MAESTRO_VERSION: "1.39.0"
+ MAESTRO_VERSION: "2.7.0"
jobs:
build:
@@ -27,16 +27,16 @@ jobs:
steps:
- name: Checkout code
- uses: actions/checkout@v5
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Java 17
- uses: actions/setup-java@v5
+ uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a
+ uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
@@ -44,7 +44,7 @@ jobs:
run: make assembleUiTestCriticalRelease
- name: Upload APK artifact
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ${{env.APK_ARTIFACT_NAME}}
path: "${{env.BASE_PATH}}/${{env.BUILD_PATH}}/${{env.APK_NAME}}"
@@ -60,24 +60,33 @@ jobs:
matrix:
include:
- api-level: 31 # Android 12
- target: aosp_atd
+ target: google_apis
channel: canary # Necessary for ATDs
arch: x86_64
+ memory: 4096
- api-level: 33 # Android 13
- target: aosp_atd
+ target: google_apis
channel: canary # Necessary for ATDs
arch: x86_64
- - api-level: 34 # Android 14
- target: aosp_atd
+ memory: 4096
+ - api-level: 35 # Android 15
+ target: google_apis
channel: canary # Necessary for ATDs
arch: x86_64
- - api-level: 35 # Android 15
- target: aosp_atd
+ memory: 4096
+ - api-level: 36 # Android 16
+ target: google_apis
+ channel: canary # Necessary for ATDs
+ arch: x86_64
+ memory: 4096
+ - api-level: "37.0" # Android 17; API 37 ships only as a minor-versioned image
+ target: google_apis_ps16k # API 37 has no plain google_apis image
channel: canary # Necessary for ATDs
arch: x86_64
+ memory: 8192
steps:
- name: Checkout code
- uses: actions/checkout@v5
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Enable KVM
run: |
@@ -85,18 +94,36 @@ jobs:
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
+ # The runner ships an outdated avdmanager that writes target=android-0 into the
+ # AVD config for minor-versioned packages (android-37.x), so the emulator clamps
+ # to API 3 and boots misconfigured. Update cmdline-tools so avdmanager parses it.
+ # See https://github.com/ReactiveCircus/android-emulator-runner/issues/482
+ - name: Update SDK cmdline-tools
+ id: cmdline-tools
+ run: |
+ SDK="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-/usr/local/lib/android/sdk}}"
+ yes | "$SDK/cmdline-tools/latest/bin/sdkmanager" --install "cmdline-tools;latest" > /dev/null
+ # sdkmanager won't overwrite the preinstalled dir, so it installs to latest-2.
+ if [ -d "$SDK/cmdline-tools/latest-2" ]; then
+ rm -rf "$SDK/cmdline-tools/latest"
+ mv "$SDK/cmdline-tools/latest-2" "$SDK/cmdline-tools/latest"
+ fi
+ echo "version=$("$SDK/cmdline-tools/latest/bin/sdkmanager" --version 2>/dev/null | grep -Eo '^[0-9][0-9.]*' | head -1)" >> "$GITHUB_OUTPUT"
+
- name: AVD cache
- uses: actions/cache@v4
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
- key: avd-api-${{ matrix.api-level }}-${{ matrix.arch }}-${{ matrix.target }}
+ # Keyed on memory and the cmdline-tools version so incompatible snapshots
+ # and AVDs created by the old, broken avdmanager are invalidated automatically.
+ key: avd-api-${{ matrix.api-level }}-${{ matrix.arch }}-${{ matrix.target }}-memory${{ matrix.memory }}-tools${{ steps.cmdline-tools.outputs.version }}
- name: Create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
- uses: reactivecircus/android-emulator-runner@1dcd0090116d15e7c562f8db72807de5e036a4ed # pin@v2
+ uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # pin@v2
with:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
@@ -105,12 +132,12 @@ jobs:
force-avd-creation: false
disable-animations: true
disable-spellchecker: true
- emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
+ emulator-options: -memory ${{ matrix.memory }} -no-window -gpu auto -noaudio -no-boot-anim -camera-back none
disk-size: 4096M
script: echo "Generated AVD snapshot for caching."
- name: Download APK artifact
- uses: actions/download-artifact@v5
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{env.APK_ARTIFACT_NAME}}
@@ -120,7 +147,7 @@ jobs:
version: ${{env.MAESTRO_VERSION}}
- name: Run tests
- uses: reactivecircus/android-emulator-runner@1dcd0090116d15e7c562f8db72807de5e036a4ed # pin@v2.34.0
+ uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # pin@v2.38.0
with:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
@@ -129,16 +156,16 @@ jobs:
force-avd-creation: false
disable-animations: true
disable-spellchecker: true
- emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -no-snapshot-save
+ emulator-options: -memory ${{ matrix.memory }} -no-window -gpu auto -noaudio -no-boot-anim -camera-back none -no-snapshot-save
script: |
adb uninstall io.sentry.uitest.android.critical || echo "Already uninstalled (or not found)"
adb install -r -d "${{env.APK_NAME}}"
- maestro test "${{env.BASE_PATH}}/maestro" --debug-output "${{env.BASE_PATH}}/maestro-logs"
+ mkdir "${{env.BASE_PATH}}/maestro-logs/" || true; adb emu screenrecord start --time-limit 360 "${{env.BASE_PATH}}/maestro-logs/recording.webm" || true; maestro test "${{env.BASE_PATH}}/maestro" --test-output-dir="${{env.BASE_PATH}}/maestro-logs/test-output" || MAESTRO_EXIT_CODE=$?; adb emu screenrecord stop || true; adb logcat -d > "${{env.BASE_PATH}}/maestro-logs/logcat.txt" || true; exit ${MAESTRO_EXIT_CODE:-0}
- name: Upload Maestro test results
- if: failure()
- uses: actions/upload-artifact@v4
+ if: ${{ always() }}
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
- name: maestro-logs
+ name: maestro-logs-${{ matrix.api-level }}-${{ matrix.arch }}-${{ matrix.target }}
path: "${{env.BASE_PATH}}/maestro-logs"
retention-days: 1
diff --git a/.github/workflows/integration-tests-ui.yml b/.github/workflows/integration-tests-ui.yml
index aee2808f529..043c4730f32 100644
--- a/.github/workflows/integration-tests-ui.yml
+++ b/.github/workflows/integration-tests-ui.yml
@@ -22,18 +22,18 @@ jobs:
steps:
- name: Git checkout
- uses: actions/checkout@v5
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
submodules: 'recursive'
- name: 'Set up Java: 17'
- uses: actions/setup-java@v5
+ uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a
+ uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
@@ -43,7 +43,7 @@ jobs:
run: make assembleUiTests
- name: Install SauceLabs CLI
- uses: saucelabs/saucectl-run-action@39e4f0666ca8ecb4b60847213c6e0fbd6a0c2bd8 # pin@v4.3.0
+ uses: saucelabs/saucectl-run-action@283660aa934c02723c497efa151d582a3acc5801 # pin@v4.5.0
env:
GITHUB_TOKEN: ${{ github.token }}
with:
@@ -73,9 +73,25 @@ jobs:
if: env.SAUCE_USERNAME != null
- - name: Upload test results to Codecov
- if: ${{ !cancelled() }}
- uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f
- with:
- token: ${{ secrets.CODECOV_TOKEN }}
- file: ./artifacts/*.xml
+ - name: Install Sentry CLI
+ if: ${{ !cancelled() && env.SAUCE_USERNAME != null }}
+ uses: getsentry/action-setup-cli@70d7e587b84c2e78cf4d37cd33d7b74fb3729c1b # v1
+
+ - name: Upload Replay Snapshots to Sentry
+ # Skip on PRs from forks, which don't have access to the upload secret
+ if: ${{ !cancelled() && env.SAUCE_USERNAME != null && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
+ run: |
+ shopt -s globstar nullglob
+ pngs=(artifacts/**/*.png)
+ if [ ${#pngs[@]} -gt 0 ]; then
+ mkdir -p replay-snapshots
+ cp "${pngs[@]}" replay-snapshots/
+ sentry-cli snapshots upload ./replay-snapshots \
+ --app-id sentry-android-replay
+ else
+ echo "No replay snapshot files found, skipping upload"
+ fi
+ env:
+ SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
+ SENTRY_ORG: sentry-sdks
+ SENTRY_PROJECT: sentry-android
diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml
index 174c9a27779..a1f47577c5f 100644
--- a/.github/workflows/release-build.yml
+++ b/.github/workflows/release-build.yml
@@ -15,24 +15,24 @@ jobs:
steps:
- name: Checkout Repo
- uses: actions/checkout@v5
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
submodules: 'recursive'
- name: Setup Java Version
- uses: actions/setup-java@v5
+ uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a
+ uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
- name: Build artifacts
run: make publish
- name: Upload artifacts
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: ${{ github.sha }}
if-no-files-found: error
diff --git a/.github/workflows/release-comment-issues.yml b/.github/workflows/release-comment-issues.yml
new file mode 100644
index 00000000000..0eeff26b9d8
--- /dev/null
+++ b/.github/workflows/release-comment-issues.yml
@@ -0,0 +1,39 @@
+name: 'Automation: Notify issues for release'
+on:
+ release:
+ types:
+ - published
+ workflow_dispatch:
+ inputs:
+ version:
+ description: Which version to notify issues for
+ required: true
+
+permissions:
+ contents: read
+ issues: write
+ pull-requests: read
+
+jobs:
+ release-comment-issues:
+ runs-on: ubuntu-24.04
+ name: 'Notify issues'
+ steps:
+ - name: Get version
+ id: get_version
+ env:
+ INPUTS_VERSION: ${{ github.event.inputs.version }}
+ RELEASE_TAG_NAME: ${{ github.event.release.tag_name }}
+ run: echo "version=${INPUTS_VERSION:-$RELEASE_TAG_NAME}" >> "$GITHUB_OUTPUT"
+
+ - name: Comment on linked issues that are mentioned in release
+ if: |
+ steps.get_version.outputs.version != ''
+ && !contains(steps.get_version.outputs.version, '-beta.')
+ && !contains(steps.get_version.outputs.version, '-alpha.')
+ && !contains(steps.get_version.outputs.version, '-rc.')
+
+ uses: getsentry/release-comment-issues-gh-action@v1
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ version: ${{ steps.get_version.outputs.version }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 1dc43dc1b13..ce4bdb23b9c 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -3,8 +3,8 @@ on:
workflow_dispatch:
inputs:
version:
- description: Version to release
- required: true
+ description: Version to release (or "auto")
+ required: false
force:
description: Force a release even when there are release-blockers (optional)
required: false
@@ -12,6 +12,10 @@ on:
description: Target branch to merge into. Uses the default branch as a fallback (optional)
required: false
+permissions:
+ contents: write
+ pull-requests: write
+
jobs:
release:
runs-on: ubuntu-latest
@@ -19,18 +23,18 @@ jobs:
steps:
- name: Get auth token
id: token
- uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
+ uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ vars.SENTRY_RELEASE_BOT_CLIENT_ID }}
private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }}
- - uses: actions/checkout@v5
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
token: ${{ steps.token.outputs.token }}
# Needs to be set, otherwise git describe --tags will fail with: No names found, cannot describe anything
fetch-depth: 0
submodules: 'recursive'
- name: Prepare release
- uses: getsentry/action-prepare-release@v1
+ uses: getsentry/craft@aeb16753a1764f3ef0768c03c499e3d2e4b7227c # v2
env:
GITHUB_TOKEN: ${{ steps.token.outputs.token }}
with:
diff --git a/.github/workflows/spring-boot-2-matrix.yml b/.github/workflows/spring-boot-2-matrix.yml
index 2e249c3fd29..66847c8c792 100644
--- a/.github/workflows/spring-boot-2-matrix.yml
+++ b/.github/workflows/spring-boot-2-matrix.yml
@@ -4,9 +4,11 @@ on:
push:
branches:
- main
- paths-ignore:
- - '**/sentry-android/**'
pull_request:
+ paths-ignore:
+ - '*android*/**'
+ - 'sentry-compose/**'
+ - 'sentry-samples/sentry-samples-android/**'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -19,7 +21,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- springboot-version: [ '2.1.0', '2.2.5', '2.4.13', '2.5.15', '2.6.15', '2.7.0', '2.7.18' ]
+ springboot-version: [ '2.4.13', '2.5.15', '2.6.15', '2.7.0', '2.7.18' ]
name: Spring Boot ${{ matrix.springboot-version }}
env:
@@ -28,12 +30,12 @@ jobs:
steps:
- name: Checkout Repo
- uses: actions/checkout@v5
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
submodules: 'recursive'
- name: Set up Python
- uses: actions/setup-python@v6
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.10.5'
@@ -43,112 +45,93 @@ jobs:
python3 -m pip install -r requirements.txt
- name: Set up Java
- uses: actions/setup-java@v5
+ uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
with:
distribution: 'temurin'
java-version: '17'
# Workaround for https://github.com/gradle/actions/issues/21 to use config cache
- name: Cache buildSrc
- uses: actions/cache@v4
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: buildSrc/build
key: build-logic-${{ hashFiles('buildSrc/src/**', 'buildSrc/build.gradle.kts','buildSrc/settings.gradle.kts') }}
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a
+ uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
- name: Update Spring Boot 2.x version
run: |
- sed -i 's/^springboot2=.*/springboot2=${{ matrix.springboot-version }}/' gradle/libs.versions.toml
- echo "Updated Spring Boot 2.x version to ${{ matrix.springboot-version }}"
-
- - name: Exclude android modules from build
- run: |
- sed -i \
- -e '/.*"sentry-android-ndk",/d' \
- -e '/.*"sentry-android",/d' \
- -e '/.*"sentry-compose",/d' \
- -e '/.*"sentry-android-core",/d' \
- -e '/.*"sentry-android-fragment",/d' \
- -e '/.*"sentry-android-navigation",/d' \
- -e '/.*"sentry-android-sqlite",/d' \
- -e '/.*"sentry-android-timber",/d' \
- -e '/.*"sentry-android-integration-tests:sentry-uitest-android-benchmark",/d' \
- -e '/.*"sentry-android-integration-tests:sentry-uitest-android",/d' \
- -e '/.*"sentry-android-integration-tests:sentry-uitest-android-critical",/d' \
- -e '/.*"sentry-android-integration-tests:test-app-sentry",/d' \
- -e '/.*"sentry-samples:sentry-samples-android",/d' \
- -e '/.*"sentry-android-replay",/d' \
- settings.gradle.kts
-
- - name: Exclude android modules from ignore list
+ springboot_version="${{ matrix.springboot-version }}"
+ if [[ ! "$springboot_version" =~ ^2\.7\. ]]; then
+ echo "ORG_GRADLE_PROJECT_excludeGraphql=true" >> "$GITHUB_ENV"
+ echo "ORG_GRADLE_PROJECT_excludeKafka=true" >> "$GITHUB_ENV"
+ fi
+ perl -0pi -e 'BEGIN { $v = shift } s/^springboot2[[:space:]]*=[[:space:]]*"\K[^"]*/$v/m or die "::error::springboot2 version entry not found in gradle/libs.versions.toml\n"' "$springboot_version" gradle/libs.versions.toml
+ echo "Updated Spring Boot 2.x version to $springboot_version"
+
+ - name: Build sample artifacts
run: |
- sed -i \
- -e '/.*"sentry-uitest-android",/d' \
- -e '/.*"sentry-uitest-android-benchmark",/d' \
- -e '/.*"sentry-uitest-android-critical",/d' \
- -e '/.*"test-app-sentry",/d' \
- -e '/.*"sentry-samples-android",/d' \
- build.gradle.kts
-
- - name: Build SDK
- run: |
- ./gradlew assemble --parallel
+ ./gradlew \
+ :sentry-samples:sentry-samples-spring-boot:shadowJar \
+ :sentry-samples:sentry-samples-spring-boot:testClasses \
+ :sentry-samples:sentry-samples-spring-boot-webflux:shadowJar \
+ :sentry-samples:sentry-samples-spring-boot-webflux:testClasses \
+ :sentry-samples:sentry-samples-spring-boot-opentelemetry:shadowJar \
+ :sentry-samples:sentry-samples-spring-boot-opentelemetry:testClasses \
+ :sentry-samples:sentry-samples-spring-boot-opentelemetry-noagent:shadowJar \
+ :sentry-samples:sentry-samples-spring-boot-opentelemetry-noagent:testClasses \
+ :sentry-samples:sentry-samples-spring:war \
+ :sentry-samples:sentry-samples-spring:testClasses \
+ :sentry-opentelemetry:sentry-opentelemetry-agent:assemble
- name: Test sentry-samples-spring-boot
run: |
python3 test/system-test-runner.py test \
--module "sentry-samples-spring-boot" \
--agent false \
- --auto-init "true" \
- --build "true"
+ --auto-init "true"
- name: Test sentry-samples-spring-boot-webflux
run: |
python3 test/system-test-runner.py test \
--module "sentry-samples-spring-boot-webflux" \
--agent false \
- --auto-init "true" \
- --build "true"
+ --auto-init "true"
- name: Test sentry-samples-spring-boot-opentelemetry agent init true
run: |
python3 test/system-test-runner.py test \
--module "sentry-samples-spring-boot-opentelemetry" \
--agent true \
- --auto-init "true" \
- --build "true"
+ --auto-init "true"
- name: Test sentry-samples-spring-boot-opentelemetry agent init false
run: |
python3 test/system-test-runner.py test \
--module "sentry-samples-spring-boot-opentelemetry" \
--agent true \
- --auto-init "false" \
- --build "true"
+ --auto-init "false"
- name: Test sentry-samples-spring-boot-opentelemetry-noagent
run: |
python3 test/system-test-runner.py test \
--module "sentry-samples-spring-boot-opentelemetry-noagent" \
--agent false \
- --auto-init "true" \
- --build "true"
+ --auto-init "true"
- name: Test sentry-samples-spring
run: |
python3 test/system-test-runner.py test \
--module "sentry-samples-spring" \
--agent false \
- --auto-init "true" \
- --build "true"
+ --auto-init "true"
- name: Upload test results
if: always()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: test-results-springboot-2-${{ matrix.springboot-version }}
path: |
@@ -158,7 +141,7 @@ jobs:
spring-server.txt
- name: Test Report
- uses: phoenix-actions/test-reporting@f957cd93fc2d848d556fa0d03c57bc79127b6b5e # pin@v15
+ uses: phoenix-actions/test-reporting@7317eea6e13c47348dd0bb318669485157c518d6 # pin@v16
if: always()
with:
name: JUnit Spring Boot 2.x ${{ matrix.springboot-version }}
@@ -167,10 +150,3 @@ jobs:
reporter: java-junit
output-to: step-summary
fail-on-error: false
-
- - name: Upload test results to Codecov
- if: ${{ !cancelled() }}
- uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f
- with:
- token: ${{ secrets.CODECOV_TOKEN }}
- file: '**/build/test-results/**/*.xml'
diff --git a/.github/workflows/spring-boot-3-matrix.yml b/.github/workflows/spring-boot-3-matrix.yml
index 3195fc9c4e8..3ccfba65c4c 100644
--- a/.github/workflows/spring-boot-3-matrix.yml
+++ b/.github/workflows/spring-boot-3-matrix.yml
@@ -4,9 +4,11 @@ on:
push:
branches:
- main
- paths-ignore:
- - '**/sentry-android/**'
pull_request:
+ paths-ignore:
+ - '*android*/**'
+ - 'sentry-compose/**'
+ - 'sentry-samples/sentry-samples-android/**'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -19,7 +21,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- springboot-version: [ '3.0.0', '3.2.10', '3.3.5', '3.4.5', '3.5.6' ]
+ springboot-version: [ '3.2.12', '3.3.13', '3.4.13', '3.5.13' ]
name: Spring Boot ${{ matrix.springboot-version }}
env:
@@ -28,12 +30,12 @@ jobs:
steps:
- name: Checkout Repo
- uses: actions/checkout@v5
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
submodules: 'recursive'
- name: Set up Python
- uses: actions/setup-python@v6
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.10.5'
@@ -43,112 +45,89 @@ jobs:
python3 -m pip install -r requirements.txt
- name: Set up Java
- uses: actions/setup-java@v5
+ uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
with:
distribution: 'temurin'
java-version: '17'
# Workaround for https://github.com/gradle/actions/issues/21 to use config cache
- name: Cache buildSrc
- uses: actions/cache@v4
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: buildSrc/build
key: build-logic-${{ hashFiles('buildSrc/src/**', 'buildSrc/build.gradle.kts','buildSrc/settings.gradle.kts') }}
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a
+ uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
- name: Update Spring Boot 3.x version
run: |
- sed -i 's/^springboot3=.*/springboot3=${{ matrix.springboot-version }}/' gradle/libs.versions.toml
- echo "Updated Spring Boot 3.x version to ${{ matrix.springboot-version }}"
+ springboot_version="${{ matrix.springboot-version }}"
+ perl -0pi -e 'BEGIN { $v = shift } s/^springboot3[[:space:]]*=[[:space:]]*"\K[^"]*/$v/m or die "::error::springboot3 version entry not found in gradle/libs.versions.toml\n"' "$springboot_version" gradle/libs.versions.toml
+ echo "Updated Spring Boot 3.x version to $springboot_version"
- - name: Exclude android modules from build
- run: |
- sed -i \
- -e '/.*"sentry-android-ndk",/d' \
- -e '/.*"sentry-android",/d' \
- -e '/.*"sentry-compose",/d' \
- -e '/.*"sentry-android-core",/d' \
- -e '/.*"sentry-android-fragment",/d' \
- -e '/.*"sentry-android-navigation",/d' \
- -e '/.*"sentry-android-sqlite",/d' \
- -e '/.*"sentry-android-timber",/d' \
- -e '/.*"sentry-android-integration-tests:sentry-uitest-android-benchmark",/d' \
- -e '/.*"sentry-android-integration-tests:sentry-uitest-android",/d' \
- -e '/.*"sentry-android-integration-tests:sentry-uitest-android-critical",/d' \
- -e '/.*"sentry-android-integration-tests:test-app-sentry",/d' \
- -e '/.*"sentry-samples:sentry-samples-android",/d' \
- -e '/.*"sentry-android-replay",/d' \
- settings.gradle.kts
-
- - name: Exclude android modules from ignore list
+ - name: Build sample artifacts
run: |
- sed -i \
- -e '/.*"sentry-uitest-android",/d' \
- -e '/.*"sentry-uitest-android-benchmark",/d' \
- -e '/.*"sentry-uitest-android-critical",/d' \
- -e '/.*"test-app-sentry",/d' \
- -e '/.*"sentry-samples-android",/d' \
- build.gradle.kts
-
- - name: Build SDK
- run: |
- ./gradlew assemble --parallel
+ ./gradlew \
+ :sentry-samples:sentry-samples-spring-boot-jakarta:bootJar \
+ :sentry-samples:sentry-samples-spring-boot-jakarta:testClasses \
+ :sentry-samples:sentry-samples-spring-boot-webflux-jakarta:bootJar \
+ :sentry-samples:sentry-samples-spring-boot-webflux-jakarta:testClasses \
+ :sentry-samples:sentry-samples-spring-boot-jakarta-opentelemetry:bootJar \
+ :sentry-samples:sentry-samples-spring-boot-jakarta-opentelemetry:testClasses \
+ :sentry-samples:sentry-samples-spring-boot-jakarta-opentelemetry-noagent:bootJar \
+ :sentry-samples:sentry-samples-spring-boot-jakarta-opentelemetry-noagent:testClasses \
+ :sentry-samples:sentry-samples-spring-jakarta:war \
+ :sentry-samples:sentry-samples-spring-jakarta:testClasses \
+ :sentry-opentelemetry:sentry-opentelemetry-agent:assemble
- name: Test sentry-samples-spring-boot-jakarta
run: |
python3 test/system-test-runner.py test \
--module "sentry-samples-spring-boot-jakarta" \
--agent false \
- --auto-init "true" \
- --build "true"
+ --auto-init "true"
- name: Test sentry-samples-spring-boot-webflux-jakarta
run: |
python3 test/system-test-runner.py test \
--module "sentry-samples-spring-boot-webflux-jakarta" \
--agent false \
- --auto-init "true" \
- --build "true"
+ --auto-init "true"
- name: Test sentry-samples-spring-boot-jakarta-opentelemetry agent init true
run: |
python3 test/system-test-runner.py test \
--module "sentry-samples-spring-boot-jakarta-opentelemetry" \
--agent true \
- --auto-init "true" \
- --build "true"
+ --auto-init "true"
- name: Test sentry-samples-spring-boot-jakarta-opentelemetry agent init false
run: |
python3 test/system-test-runner.py test \
--module "sentry-samples-spring-boot-jakarta-opentelemetry" \
--agent true \
- --auto-init "false" \
- --build "true"
+ --auto-init "false"
- name: Test sentry-samples-spring-boot-jakarta-opentelemetry-noagent
run: |
python3 test/system-test-runner.py test \
--module "sentry-samples-spring-boot-jakarta-opentelemetry-noagent" \
--agent false \
- --auto-init "true" \
- --build "true"
+ --auto-init "true"
- name: Test sentry-samples-spring-jakarta
run: |
python3 test/system-test-runner.py test \
--module "sentry-samples-spring-jakarta" \
--agent false \
- --auto-init "true" \
- --build "true"
+ --auto-init "true"
- name: Upload test results
if: always()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: test-results-springboot-3-${{ matrix.springboot-version }}
path: |
@@ -158,7 +137,7 @@ jobs:
spring-server.txt
- name: Test Report
- uses: phoenix-actions/test-reporting@f957cd93fc2d848d556fa0d03c57bc79127b6b5e # pin@v15
+ uses: phoenix-actions/test-reporting@7317eea6e13c47348dd0bb318669485157c518d6 # pin@v16
if: always()
with:
name: JUnit Spring Boot 3.x ${{ matrix.springboot-version }}
@@ -167,10 +146,3 @@ jobs:
reporter: java-junit
output-to: step-summary
fail-on-error: false
-
- - name: Upload test results to Codecov
- if: ${{ !cancelled() }}
- uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f
- with:
- token: ${{ secrets.CODECOV_TOKEN }}
- file: '**/build/test-results/**/*.xml'
diff --git a/.github/workflows/spring-boot-4-matrix.yml b/.github/workflows/spring-boot-4-matrix.yml
index 6c980e10646..f75f31e38ef 100644
--- a/.github/workflows/spring-boot-4-matrix.yml
+++ b/.github/workflows/spring-boot-4-matrix.yml
@@ -4,9 +4,11 @@ on:
push:
branches:
- main
- paths-ignore:
- - '**/sentry-android/**'
pull_request:
+ paths-ignore:
+ - '*android*/**'
+ - 'sentry-compose/**'
+ - 'sentry-samples/sentry-samples-android/**'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -19,7 +21,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- springboot-version: [ '4.0.0-M1', '4.0.0-M2', '4.0.0-M3' ]
+ springboot-version: [ '4.0.0', '4.0.5', '4.1.0' ]
name: Spring Boot ${{ matrix.springboot-version }}
env:
@@ -28,12 +30,12 @@ jobs:
steps:
- name: Checkout Repo
- uses: actions/checkout@v5
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
submodules: 'recursive'
- name: Set up Python
- uses: actions/setup-python@v6
+ uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.10.5'
@@ -43,113 +45,89 @@ jobs:
python3 -m pip install -r requirements.txt
- name: Set up Java
- uses: actions/setup-java@v5
+ uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
with:
distribution: 'temurin'
java-version: '17'
# Workaround for https://github.com/gradle/actions/issues/21 to use config cache
- name: Cache buildSrc
- uses: actions/cache@v4
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: buildSrc/build
key: build-logic-${{ hashFiles('buildSrc/src/**', 'buildSrc/build.gradle.kts','buildSrc/settings.gradle.kts') }}
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a
+ uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
- name: Update Spring Boot 4.x version
run: |
- sed -i 's/^springboot4=.*/springboot4=${{ matrix.springboot-version }}/' gradle/libs.versions.toml
- echo "Updated Spring Boot 4.x version to ${{ matrix.springboot-version }}"
+ springboot_version="${{ matrix.springboot-version }}"
+ perl -0pi -e 'BEGIN { $v = shift } s/^springboot4[[:space:]]*=[[:space:]]*"\K[^"]*/$v/m or die "::error::springboot4 version entry not found in gradle/libs.versions.toml\n"' "$springboot_version" gradle/libs.versions.toml
+ echo "Updated Spring Boot 4.x version to $springboot_version"
- - name: Exclude android modules from build
- run: |
- sed -i \
- -e '/.*"sentry-android-ndk",/d' \
- -e '/.*"sentry-android",/d' \
- -e '/.*"sentry-compose",/d' \
- -e '/.*"sentry-android-core",/d' \
- -e '/.*"sentry-android-fragment",/d' \
- -e '/.*"sentry-android-navigation",/d' \
- -e '/.*"sentry-android-sqlite",/d' \
- -e '/.*"sentry-android-timber",/d' \
- -e '/.*"sentry-android-integration-tests:sentry-uitest-android-benchmark",/d' \
- -e '/.*"sentry-android-integration-tests:sentry-uitest-android",/d' \
- -e '/.*"sentry-android-integration-tests:sentry-uitest-android-critical",/d' \
- -e '/.*"sentry-android-integration-tests:test-app-sentry",/d' \
- -e '/.*"sentry-samples:sentry-samples-android",/d' \
- -e '/.*"sentry-android-replay",/d' \
- settings.gradle.kts
-
- - name: Exclude android modules from ignore list
+ - name: Build sample artifacts
run: |
- sed -i \
- -e '/.*"sentry-uitest-android",/d' \
- -e '/.*"sentry-uitest-android-benchmark",/d' \
- -e '/.*"sentry-uitest-android-critical",/d' \
- -e '/.*"test-app-sentry",/d' \
- -e '/.*"sentry-samples-android",/d' \
- build.gradle.kts
-
- - name: Build SDK
- run: |
- ./gradlew assemble --parallel
+ ./gradlew \
+ :sentry-samples:sentry-samples-spring-boot-4:bootJar \
+ :sentry-samples:sentry-samples-spring-boot-4:testClasses \
+ :sentry-samples:sentry-samples-spring-boot-4-webflux:bootJar \
+ :sentry-samples:sentry-samples-spring-boot-4-webflux:testClasses \
+ :sentry-samples:sentry-samples-spring-boot-4-opentelemetry:bootJar \
+ :sentry-samples:sentry-samples-spring-boot-4-opentelemetry:testClasses \
+ :sentry-samples:sentry-samples-spring-boot-4-opentelemetry-noagent:bootJar \
+ :sentry-samples:sentry-samples-spring-boot-4-opentelemetry-noagent:testClasses \
+ :sentry-samples:sentry-samples-spring-7:war \
+ :sentry-samples:sentry-samples-spring-7:testClasses \
+ :sentry-opentelemetry:sentry-opentelemetry-agent:assemble
- name: Run sentry-samples-spring-boot-4
run: |
python3 test/system-test-runner.py test \
--module "sentry-samples-spring-boot-4" \
--agent false \
- --auto-init "true" \
- --build "true"
+ --auto-init "true"
- name: Run sentry-samples-spring-boot-4-webflux
run: |
python3 test/system-test-runner.py test \
--module "sentry-samples-spring-boot-4-webflux" \
--agent false \
- --auto-init "true" \
- --build "true"
+ --auto-init "true"
- name: Run sentry-samples-spring-boot-4-opentelemetry agent init true
run: |
python3 test/system-test-runner.py test \
--module "sentry-samples-spring-boot-4-opentelemetry" \
--agent true \
- --auto-init "true" \
- --build "true"
+ --auto-init "true"
- name: Run sentry-samples-spring-boot-4-opentelemetry agent init false
run: |
python3 test/system-test-runner.py test \
--module "sentry-samples-spring-boot-4-opentelemetry" \
--agent true \
- --auto-init "false" \
- --build "true"
-
-# needs a fix in opentelemetry-spring-boot-starter
-# - name: Run sentry-samples-spring-boot-4-opentelemetry-noagent
-# run: |
-# python3 test/system-test-runner.py test \
-# --module "sentry-samples-spring-boot-4-opentelemetry-noagent" \
-# --agent false \
-# --auto-init "true" \
-# --build "true"
+ --auto-init "false"
+
+ - name: Run sentry-samples-spring-boot-4-opentelemetry-noagent
+ run: |
+ python3 test/system-test-runner.py test \
+ --module "sentry-samples-spring-boot-4-opentelemetry-noagent" \
+ --agent false \
+ --auto-init "true"
- name: Run sentry-samples-spring-7
run: |
python3 test/system-test-runner.py test \
--module "sentry-samples-spring-7" \
--agent false \
- --auto-init "true" \
- --build "true"
+ --auto-init "true"
- name: Upload test results
if: always()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: test-results-springboot-4-${{ matrix.springboot-version }}
path: |
@@ -159,7 +137,7 @@ jobs:
spring-server.txt
- name: Test Report
- uses: phoenix-actions/test-reporting@f957cd93fc2d848d556fa0d03c57bc79127b6b5e # pin@v15
+ uses: phoenix-actions/test-reporting@7317eea6e13c47348dd0bb318669485157c518d6 # pin@v16
if: always()
with:
name: JUnit Spring Boot 4.x ${{ matrix.springboot-version }}
@@ -168,10 +146,3 @@ jobs:
reporter: java-junit
output-to: step-summary
fail-on-error: false
-
- - name: Upload test results to Codecov
- if: ${{ !cancelled() }}
- uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f
- with:
- token: ${{ secrets.CODECOV_TOKEN }}
- file: '**/build/test-results/**/*.xml'
diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml
index 43f69a69889..12d84c0ef99 100644
--- a/.github/workflows/system-tests-backend.yml
+++ b/.github/workflows/system-tests-backend.yml
@@ -5,6 +5,10 @@ on:
branches:
- main
pull_request:
+ paths-ignore:
+ - '*android*/**'
+ - 'sentry-compose/**'
+ - 'sentry-samples/sentry-samples-android/**'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -54,6 +58,9 @@ jobs:
- sample: "sentry-samples-console"
agent: "false"
agent-auto-init: "true"
+ - sample: "sentry-samples-console-otlp"
+ agent: "false"
+ agent-auto-init: "true"
- sample: "sentry-samples-logback"
agent: "false"
agent-auto-init: "true"
@@ -69,15 +76,18 @@ jobs:
- sample: "sentry-samples-spring-boot-4-webflux"
agent: "false"
agent-auto-init: "true"
-# - sample: "sentry-samples-spring-boot-4-opentelemetry-noagent"
-# agent: "false"
-# agent-auto-init: "true"
+ - sample: "sentry-samples-spring-boot-4-opentelemetry-noagent"
+ agent: "false"
+ agent-auto-init: "true"
- sample: "sentry-samples-spring-boot-4-opentelemetry"
agent: "true"
agent-auto-init: "true"
- sample: "sentry-samples-spring-boot-4-opentelemetry"
agent: "true"
agent-auto-init: "false"
+ - sample: "sentry-samples-spring-boot-4-otlp"
+ agent: "false"
+ agent-auto-init: "true"
- sample: "sentry-samples-spring-7"
agent: "false"
agent-auto-init: "true"
@@ -88,11 +98,11 @@ jobs:
agent: "false"
agent-auto-init: "true"
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
submodules: 'recursive'
- - uses: actions/setup-python@v6
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.10.5'
@@ -102,52 +112,23 @@ jobs:
python3 -m pip install -r requirements.txt
- name: Set up Java
- uses: actions/setup-java@v5
+ uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Gradle
- uses: gradle/actions/setup-gradle@ed408507eac070d1f99cc633dbcf757c94c7933a
+ uses: gradle/actions/setup-gradle@9c971963bec38e04b3d30dcc455b5382be2fdbfb # v6.3.0
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
- - name: Exclude android modules from build
- run: |
- sed -i \
- -e '/.*"sentry-android-ndk",/d' \
- -e '/.*"sentry-android",/d' \
- -e '/.*"sentry-compose",/d' \
- -e '/.*"sentry-android-core",/d' \
- -e '/.*"sentry-android-fragment",/d' \
- -e '/.*"sentry-android-navigation",/d' \
- -e '/.*"sentry-android-sqlite",/d' \
- -e '/.*"sentry-android-timber",/d' \
- -e '/.*"sentry-android-integration-tests:sentry-uitest-android-benchmark",/d' \
- -e '/.*"sentry-android-integration-tests:sentry-uitest-android",/d' \
- -e '/.*"sentry-android-integration-tests:sentry-uitest-android-critical",/d' \
- -e '/.*"sentry-android-integration-tests:test-app-sentry",/d' \
- -e '/.*"sentry-samples:sentry-samples-android",/d' \
- -e '/.*"sentry-android-replay",/d' \
- settings.gradle.kts
-
- - name: Exclude android modules from ignore list
- run: |
- sed -i \
- -e '/.*"sentry-uitest-android",/d' \
- -e '/.*"sentry-uitest-android-benchmark",/d' \
- -e '/.*"sentry-uitest-android-critical",/d' \
- -e '/.*"test-app-sentry",/d' \
- -e '/.*"sentry-samples-android",/d' \
- build.gradle.kts
-
- name: Build and run system tests
run: |
python3 test/system-test-runner.py test --module "${{ matrix.sample }}" --agent "${{ matrix.agent }}" --auto-init "${{ matrix.agent-auto-init }}" --build "true"
- name: Upload test results
if: always()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: test-results-${{ matrix.sample }}-${{ matrix.agent }}-${{ matrix.agent-auto-init }}-system-test
path: |
@@ -156,7 +137,7 @@ jobs:
spring-server.txt
- name: Test Report
- uses: phoenix-actions/test-reporting@f957cd93fc2d848d556fa0d03c57bc79127b6b5e # pin@v15
+ uses: phoenix-actions/test-reporting@7317eea6e13c47348dd0bb318669485157c518d6 # pin@v16
if: always()
with:
name: JUnit System Tests ${{ matrix.sample }}
diff --git a/.github/workflows/update-deps.yml b/.github/workflows/update-deps.yml
index 83d90bb9199..5b8d3d11628 100644
--- a/.github/workflows/update-deps.yml
+++ b/.github/workflows/update-deps.yml
@@ -9,21 +9,17 @@ on:
branches:
- main
+permissions:
+ contents: write
+ pull-requests: write
+ actions: write
+
jobs:
native:
- uses: getsentry/github-workflows/.github/workflows/updater.yml@v2
- with:
- path: scripts/update-sentry-native-ndk.sh
- name: Native SDK
- secrets:
- # If a custom token is used instead, a CI would be triggered on a created PR.
- api-token: ${{ secrets.CI_DEPLOY_KEY }}
-
- gradle-wrapper:
- uses: getsentry/github-workflows/.github/workflows/updater.yml@v2
- with:
- path: scripts/update-gradle.sh
- name: Gradle
- pattern: '^v[0-9.]+$' # only match non-preview versions
- secrets:
- api-token: ${{ secrets.CI_DEPLOY_KEY }}
+ runs-on: ubuntu-latest
+ steps:
+ - uses: getsentry/github-workflows/updater@607fed74f812e69201531a5185b6c3c57caa4e89 # v3
+ with:
+ path: scripts/update-sentry-native-ndk.sh
+ name: Native SDK
+ ssh-key: ${{ secrets.CI_DEPLOY_KEY }}
diff --git a/.github/workflows/validate-pr.yml b/.github/workflows/validate-pr.yml
new file mode 100644
index 00000000000..ca5108943de
--- /dev/null
+++ b/.github/workflows/validate-pr.yml
@@ -0,0 +1,16 @@
+name: Validate PR
+
+on:
+ pull_request_target:
+ types: [opened, reopened]
+
+jobs:
+ validate-pr:
+ runs-on: ubuntu-24.04
+ permissions:
+ pull-requests: write
+ steps:
+ - uses: getsentry/github-workflows/validate-pr@607fed74f812e69201531a5185b6c3c57caa4e89 # v3
+ with:
+ app-id: ${{ vars.SDK_MAINTAINER_BOT_APP_ID }}
+ private-key: ${{ secrets.SDK_MAINTAINER_BOT_PRIVATE_KEY }}
diff --git a/.gitignore b/.gitignore
index be4f11ce3d2..f252087a5ab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
.DS_Store
+.java-version
.idea/
.gradle/
.run/
@@ -12,6 +13,7 @@ local.properties
**/sentry-native-local
target/
.classpath
+.factorypath
.project
.settings/
bin/
@@ -27,3 +29,14 @@ spring-server.txt
spy.log
.kotlin
**/tomcat.8080/webapps/
+**/__pycache__
+
+# Local Claude Code settings/state that should not be committed
+.claude/settings.local.json
+.claude/worktrees/
+# Auto-generated by dotagents — do not commit these files.
+agents.lock
+.agents/.gitignore
+
+# Warden local run logs
+.warden/logs/
diff --git a/.pi/settings.json b/.pi/settings.json
new file mode 100644
index 00000000000..e614d527837
--- /dev/null
+++ b/.pi/settings.json
@@ -0,0 +1,6 @@
+{
+ "skills": [
+ "../.claude/skills"
+ ],
+ "enableSkillCommands": true
+}
diff --git a/.python-version b/.python-version
new file mode 100644
index 00000000000..2c20ac9bea3
--- /dev/null
+++ b/.python-version
@@ -0,0 +1 @@
+3.13.3
diff --git a/.sauce/sentry-uitest-android-ui.yml b/.sauce/sentry-uitest-android-ui.yml
index 8d84f865c95..a00ee10614b 100644
--- a/.sauce/sentry-uitest-android-ui.yml
+++ b/.sauce/sentry-uitest-android-ui.yml
@@ -32,4 +32,5 @@ artifacts:
when: always
match:
- junit.xml
+ - "*.png"
directory: ./artifacts/
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 00000000000..42bc6677d17
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,237 @@
+# AGENTS.md
+
+This file provides guidance to AI coding agents when working with code in this repository.
+
+## Domain-Specific Rules
+
+This file covers the whole repository. Before working on a specific area, read the matching
+rule file in `.cursor/rules/`:
+
+| Rule | Read it when working on |
+|---|---|
+| `api` | Public API surface, binary compatibility, `.api` files, `apiDump`, `IScope`/`IScopes`/`Sentry` static API, protocol classes |
+| `options` | `SentryOptions`, namespaced options, `ExternalOptions`, `sentry.properties`, `ManifestMetadataReader`, Spring Boot properties |
+| `scopes` | Scope management, forking, lifecycle, `ScopeType`, thread-local storage, scope bleeding, Hub → Scopes migration |
+| `deduplication` | Duplicate event detection, `DuplicateEventDetectionEventProcessor`, `enableDeduplication` |
+| `offline` | Caching, envelope storage, network failure handling, retries, `AsyncHttpTransport`, `EnvelopeCache`, rate limiting |
+| `feature_flags` | `addFeatureFlag`, `FeatureFlagBuffer`, `maxFeatureFlags`, LaunchDarkly and OpenFeature integrations |
+| `metrics` | `Sentry.metrics()`, `IMetricsApi`, count/distribution/gauge, `MetricsBatchProcessor` |
+| `queues` | Queue tracing, `queue.publish`/`queue.process`, `enableQueueTracing`, Kafka instrumentation, messaging span data |
+| `continuous_profiling_jvm` | `sentry-async-profiler`, `IContinuousProfiler`, `ProfileChunk`, JFR files, `ProfileLifecycle` |
+| `opentelemetry` | `sentry-opentelemetry-*`, agent vs agentless, span processing, sampling, context propagation |
+| `new_module` | Adding a new integration or sample module |
+| `e2e_tests` | System tests, sample applications, `system-test-runner.py`, mock Sentry server |
+
+Rules can be combined — a tracing scope issue may need both `scopes` and `opentelemetry`.
+There is no rule for Android profiling yet; read the `sentry-android-core` profiling code
+directly and fetch related rules such as `options`, `offline`, or `api` as needed.
+
+## Project Overview
+
+This is the Sentry Java/Android SDK - a comprehensive error monitoring and performance tracking SDK for Java and Android applications. The repository contains multiple modules for different integrations and platforms.
+
+## Build System
+
+The project uses **Gradle** with Kotlin DSL. Key build files:
+- `build.gradle.kts` - Root build configuration
+- `settings.gradle.kts` - Multi-module project structure
+- `buildSrc/` and `build-logic/` - Custom build logic and plugins
+- `Makefile` - High-level build commands
+
+## Essential Commands
+
+### Development Workflow
+```bash
+# Format code and regenerate .api files (REQUIRED before committing)
+./gradlew spotlessApply apiDump
+
+# Run all tests and linter
+./gradlew check
+
+# Generate documentation
+./gradlew aggregateJavadocs
+```
+
+### Testing
+```bash
+# Run unit tests for a specific file
+./gradlew '::testReleaseUnitTest' --tests="**" --info
+
+# Run system tests (requires Python virtual env)
+make systemTest
+
+# Run specific test suites
+./gradlew :sentry-android-core:testReleaseUnitTest
+./gradlew :sentry:test
+```
+
+### Code Quality
+```bash
+# Check code formatting
+./gradlew spotlessJavaCheck spotlessKotlinCheck
+
+# Apply code formatting
+./gradlew spotlessApply
+
+# Update API dump files (after API changes)
+./gradlew apiDump
+
+# Dependency updates check
+./gradlew dependencyUpdates -Drevision=release
+```
+
+### Android-Specific Commands
+```bash
+# Assemble Android test APKs
+./gradlew :sentry-android-integration-tests:sentry-uitest-android:assembleRelease :sentry-android-integration-tests:sentry-uitest-android:assembleAndroidTest
+
+# Run critical UI tests
+./scripts/test-ui-critical.sh
+```
+
+## Development Workflow Rules
+
+### Planning and Implementation Process
+1. **First think through the problem**: Read the codebase for relevant files and propose a plan
+2. **Check in before beginning**: Verify the plan before starting implementation
+3. **Use todo tracking**: Work through todo items, marking them as complete as you go
+4. **High-level communication**: Give high-level explanations of changes made, not step-by-step descriptions
+5. **Simplicity first**: Make every task and code change as simple as possible. Avoid massive or complex changes. Impact as little code as possible.
+6. **Format and regenerate**: Once done, format code and regenerate .api files: `./gradlew spotlessApply apiDump`
+7. **Propose commit**: As final step, git stage relevant files and propose (but not execute) a single git commit command. This applies to implementation work; when the task is to open a PR, the `create-java-pr` skill takes over from here and does commit, push, and open it.
+
+## Repository Skills
+
+This repo ships task-specific skills (declared in `agents.toml`, sources under `.agents/skills`). Prefer them over performing the steps manually:
+- **`create-java-pr`**: Branch, format, `apiDump`, commit, push, open PR, and add the changelog entry (automates the PR workflow above)
+- **`test`**: Run unit or system tests for a module or a specific class
+- **`check-code-attribution`**: Verify third-party code attribution on the current branch (see Third-Party Code Attribution below)
+- **`btrace-perfetto`**: Capture and compare Perfetto traces for Android performance work
+
+## Module Architecture
+
+The repository is organized into multiple modules:
+
+### Core Modules
+- **`sentry`** - Core Java SDK implementation
+- **`sentry-android-core`** - Core Android SDK implementation
+- **`sentry-android`** - High-level Android SDK
+- **`sentry-android-ndk`** - Native (NDK) crash handling
+
+### Integration Modules
+- **Spring Framework**: `sentry-spring*`, `sentry-spring-boot*`
+- **Logging**: `sentry-logback`, `sentry-log4j2`, `sentry-jul`, `sentry-android-timber`
+- **Web**: `sentry-servlet*`, `sentry-okhttp`, `sentry-openfeign`, `sentry-apache-http-client-5`
+- **GraphQL**: `sentry-graphql*`, `sentry-apollo*`
+- **Android UI**: `sentry-android-fragment`, `sentry-android-navigation`, `sentry-compose`
+- **Session Replay**: `sentry-android-replay`
+- **Database**: `sentry-jdbc`, `sentry-android-sqlite`, `sentry-jcache`
+- **Reactive**: `sentry-reactor`, `sentry-ktor-client`
+- **Feature Flags**: `sentry-launchdarkly-android`, `sentry-launchdarkly-server`, `sentry-openfeature`
+- **Queues**: `sentry-kafka`
+- **Profiling**: `sentry-async-profiler` (JVM continuous profiling)
+- **Monitoring**: `sentry-opentelemetry*`, `sentry-quartz`
+- **Other**: `sentry-spotlight`, `sentry-kotlin-extensions`, `sentry-android-distribution`
+
+### Utility Modules
+- **`sentry-test-support`** - Shared test utilities
+- **`sentry-system-test-support`** - System testing infrastructure
+- **`sentry-samples`** - Example applications
+- **`sentry-bom`** - Bill of Materials for dependency management
+
+### Key Architectural Patterns
+- **Multi-platform**: Supports JVM, Android, and Kotlin Multiplatform (Compose modules)
+- **Modular Design**: Each integration is a separate module with minimal dependencies
+- **Options Pattern**: Features are opt-in via `SentryOptions` and similar configuration classes
+- **Transport Layer**: Pluggable transport implementations for different environments
+- **Scope Management**: Thread-safe scope/context management for error tracking
+
+## Development Guidelines
+
+### Code Style
+- **Languages**: Java 8+ and Kotlin
+- **Formatting**: Enforced via Spotless - always run `./gradlew spotlessApply` before committing
+- **API Compatibility**: Binary compatibility is enforced - run `./gradlew apiDump` after API changes
+
+### Exception Handling
+
+**Never introduce a new `catch (Throwable)`.** Catch the narrowest type the guarded code can
+actually throw. The repository still contains many pre-existing broad catches; they are legacy,
+not a precedent to follow.
+
+A broad catch swallows `OutOfMemoryError`, `StackOverflowError`, `ThreadDeath` and `LinkageError` —
+conditions the JVM/ART cannot recover from and that leave the process in an undefined state — and
+it hides real bugs in our own code behind a log line.
+
+"The SDK must never crash the host application" is not a reason to catch `Throwable`. That goal is
+served by `io.sentry.util.ExceptionUtils.rethrowIfFatal`, which lets the non-recoverable throwables
+through while leaving everything else for the caller to log or ignore:
+
+```java
+try {
+ doSomethingRisky();
+} catch (Throwable t) {
+ ExceptionUtils.rethrowIfFatal(t);
+ options.getLogger().log(SentryLevel.ERROR, "Failed to do something risky", t);
+}
+```
+
+Apply that pattern only where a broad catch is genuinely unavoidable — an entry point that runs
+arbitrary user code or third-party callbacks. Everywhere else, name the exception types. Say in the
+PR description why the broad catch is necessary.
+
+### Testing Requirements
+- Write comprehensive unit tests for new features
+- Android modules require both unit tests and instrumented tests where applicable
+- System tests validate end-to-end functionality with sample applications
+- **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.
+- 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`.
+
+### Contributing Guidelines
+1. Follow existing code style and language
+2. Do not modify API files (e.g. sentry.api) manually - run `./gradlew apiDump` to regenerate them
+3. Write comprehensive tests
+4. New features must be **opt-in by default** - extend `SentryOptions` or similar Option classes with getters/setters
+5. Consider backwards compatibility
+
+### Third-Party Code Attribution
+When adapting code from third-party libraries:
+1. Add a license header at the top of the adapted file (before the `package` statement):
+ ```java
+ // Adapted from .
+ // Copyright .
+ // Licensed under the .
+ //
+ ```
+2. Add a full attribution entry to `THIRD_PARTY_NOTICES.md` following the existing format (Source, License, Copyright, Scope, full license text)
+
+3. Run the `check-code-attribution` skill locally or wait for it to be auto-run against your PR to check for required fields and verify new licenses against [Sentry's Open Source Legal Policy](https://open.sentry.io/licensing/).
+
+### Getting PR Information
+
+Use `gh pr view` to get PR details from the current branch. This is needed when adding changelog entries, which require the PR number.
+
+```bash
+# Get PR number for current branch
+gh pr view --json number -q '.number'
+
+# Get PR number for a specific branch
+gh pr view --json number -q '.number'
+
+# Get PR URL
+gh pr view --json url -q '.url'
+```
+
+### Changelog
+
+User-facing changes get an entry under the `## Unreleased` section of `CHANGELOG.md`. The
+`create-java-pr` skill is the source of truth for the full changelog and PR workflow, including
+subsection selection and the rebase caveat when a release renames `## Unreleased`.
+
+## Useful Resources
+
+- Main SDK documentation: https://develop.sentry.dev/sdk/overview/
+- Internal contributing guide: https://docs.sentry.io/internal/contributing/
+- Git commit message conventions: https://develop.sentry.dev/engineering-practices/commit-messages/
+
+This SDK is production-ready and used by thousands of applications. Changes should be thoroughly tested and maintain backwards compatibility.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7f2b745f91a..0a45ec3fe37 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,1083 @@
## Unreleased
+### Dependencies
+
+- Bump Native SDK from v0.16.2 to v0.16.3 ([#5962](https://github.com/getsentry/sentry-java/pull/5962))
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0163)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.16.2...0.16.3)
+
+## 8.53.0
+
+### Features
+
+- Allow child spans to use explicit start timestamps through `ISpan` ([#5929](https://github.com/getsentry/sentry-java/pull/5929))
+- Make `ISpan.startChild` overloads with `SpanOptions` public ([#5927](https://github.com/getsentry/sentry-java/pull/5927))
+- Add `Sentry.feedback().enableOnShake()`, `Sentry.feedback().disableOnShake()`, and `Sentry.feedback().isOnShakeEnabled()` to toggle and query shake-to-report at runtime ([#5827](https://github.com/getsentry/sentry-java/pull/5827))
+
+### Improvements
+
+- Remove `ApiStatus.Experimental` annotation from `SentrySQLiteDriver` ([#5938](https://github.com/getsentry/sentry-java/pull/5938))
+
+### Fixes
+
+- Clear contexts when calling `Scope.clear()` ([#5902](https://github.com/getsentry/sentry-java/pull/5902))
+- Preserve custom `Throwable` identities when R8 optimizes Android apps ([#5881](https://github.com/getsentry/sentry-java/pull/5881))
+- Report the correct cpu usage for the first performance sample of a transaction, which was measured against the time since device boot ([#5926](https://github.com/getsentry/sentry-java/pull/5926))
+- Prevent an ANR when the Session Replay video encoder gets stuck ([#5842](https://github.com/getsentry/sentry-java/pull/5842))
+ - Some hardware encoders never signal end-of-stream, which made the replay worker spin forever while holding the encoder lock. The app's lifecycle callbacks then blocked on that lock and the app froze until the system killed it. The encoder now gives up instead of spinning, and closing the replay cache no longer waits indefinitely for a wedged encoder.
+
+### Performance
+
+- Read the clock once per performance collection round instead of once per in-flight transaction ([#5934](https://github.com/getsentry/sentry-java/pull/5934))
+- Reduce allocations while collecting cpu usage during transactions by reading the process cpu time via `Process.getElapsedCpuTime()` instead of parsing `/proc/self/stat` (33.6kB to 16 bytes per sample on a Pixel 3) ([#5926](https://github.com/getsentry/sentry-java/pull/5926))
+- Store performance measurements as primitives, removing a boxed allocation per measurement per performance sample ([#5935](https://github.com/getsentry/sentry-java/pull/5935))
+
+### Dependencies
+
+- Bump Native SDK from v0.16.1 to v0.16.2 ([#5910](https://github.com/getsentry/sentry-java/pull/5910))
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0162)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.16.1...0.16.2)
+
+## 8.52.0
+
+### Fixes
+
+- Restore the interrupt flag when cached envelope processing is interrupted between files ([#5884](https://github.com/getsentry/sentry-java/pull/5884))
+- Reduce false-positive SDK crash attribution for host app SQLite cursor crashes ([#5883](https://github.com/getsentry/sentry-java/pull/5883))
+- Prevent inflated cold app start when the OS spawns the process in the background (e.g. FCM push) on API 35+ ([#5841](https://github.com/getsentry/sentry-java/pull/5841), [#5880](https://github.com/getsentry/sentry-java/pull/5880))
+- Preserve single-sample ANR profile chunks so profiles remain available on ANR events ([#5872](https://github.com/getsentry/sentry-java/pull/5872))
+- Avoid a CPU busy-loop when recording discarded log or metric envelopes under rate limiting ([#5835](https://github.com/getsentry/sentry-java/pull/5835))
+ - `ClientReportRecorder` now reads the item count from the envelope item header instead of deserializing the payload, which under sustained rate limiting could pin CPU cores while repeatedly throwing exceptions
+- Report tasks handed to a no-op `ISentryExecutorService` as cancelled ([#5874](https://github.com/getsentry/sentry-java/pull/5874))
+ - `NoOpSentryExecutorService` previously returned a `Future` that was never run and never cancelled, so callers could not tell a dropped task from a queued one and `get()` would block until its timeout
+
+### Performance
+
+- Defer use of reflection by `SentryFrameMetricsCollector` during `Sentry.init` ([#5886](https://github.com/getsentry/sentry-java/pull/5886))
+- Avoid waiting up to `shutdownTimeoutMillis` when closing the SDK with a pending transaction timeout or session-end task ([#5851](https://github.com/getsentry/sentry-java/pull/5851))
+- Use `RGB_565` instead of `ARGB_8888` for screenshot and replay capture bitmaps, halving per-frame memory usage ([#5821](https://github.com/getsentry/sentry-java/pull/5821))
+- Remove an unused lock from `SentryPerformanceProvider`, which was allocated on every cold start in `ContentProvider.onCreate` without ever being acquired ([#5871](https://github.com/getsentry/sentry-java/pull/5871))
+- Reduce main-thread allocations when parsing the app start profiling config ([#5867](https://github.com/getsentry/sentry-java/pull/5867))
+- Batch and coalesce scope-persistence disk writes to reduce startup cost ([#5791](https://github.com/getsentry/sentry-java/pull/5791))
+ - Scope mutations are now coalesced (latest value per field) and breadcrumbs are appended in batches behind a single fsync, instead of one synchronous disk write per mutation.
+- Reduce the number of SDK threads: the `HostnameCache` worker thread now times out while idle instead of staying alive for the whole process lifetime ([#5817](https://github.com/getsentry/sentry-java/pull/5817))
+
+### Dependencies
+
+- Bump Native SDK from v0.16.0 to v0.16.1 ([#5879](https://github.com/getsentry/sentry-java/pull/5879))
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0161)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.16.0...0.16.1)
+
+## 8.51.0
+
+### Features
+
+- Use Android's `ProfilingManager` (Perfetto) for continuous profiling on API 35+ devices ([#5251](https://github.com/getsentry/sentry-java/pull/5251))
+ - On API 35+ devices, continuous profiling now automatically uses Android's system `ProfilingManager` with Perfetto-based stack sampling, providing lower-overhead and more accurate profiles. No configuration change is required.
+ - Devices below API 35 keep using the legacy `Debug`-based profiler.
+ - Added an `enableLegacyProfiling` option (default `true`) to disable the legacy `Debug`-based profiler. Setting it to `false` disables continuous profiling on API < 35 devices as well as transaction-based profiling (`profilesSampleRate`/`profilesSampler`) on all devices, since transaction-based profiling is not supported by Perfetto.
+ - It can also be configured via the `io.sentry.profiling.enable-legacy-profiling` manifest flag.
+ - See the [Android profiling docs](https://docs.sentry.io/platforms/android/profiling/) for details.
+
+### Behavioral Changes
+
+- The outbox and cache directories are no longer created by `Sentry.init` ([#5792](https://github.com/getsentry/sentry-java/pull/5792))
+ - They are now created lazily by whichever component first writes into them, off the init thread. As a result, the directories at `SentryOptions.getOutboxPath()` and `SentryOptions.getCacheDirPath()` are not guaranteed to exist once `Sentry.init` returns.
+ - If you write envelopes into the outbox path yourself instead of going through the SDK — as hybrid SDKs do for `captureEnvelope` — create the directory first, e.g. `new File(outboxPath).mkdirs()`.
+
+### Improvements
+
+- Skip building Android manifest metadata debug log messages when debug logging is disabled, reducing allocations during SDK init ([#5790](https://github.com/getsentry/sentry-java/pull/5790))
+
+### Fixes
+
+- Use the original app build's ProGuard UUID for ANR profile chunks ([#5852](https://github.com/getsentry/sentry-java/pull/5852))
+- Fix potential ANR/deadlock in Session Replay when `checkCanRecord` runs on the replay executor thread ([#5837](https://github.com/getsentry/sentry-java/pull/5837))
+- Prevent concurrent PixelCopy access during Session Replay masking and bitmap cleanup ([#5808](https://github.com/getsentry/sentry-java/pull/5808))
+- Release `MediaMuxer` when the replay video encoder fails to start to avoid a resource leak ([#5607](https://github.com/getsentry/sentry-java/pull/5607))
+- Set the correct platform (`android` instead of `java`) on ANR profile chunks so they are billed as UI Profile Hours rather than Continuous Profile Hours ([#5836](https://github.com/getsentry/sentry-java/pull/5836))
+- Skip encoding and capturing buffered session replay segments while rate-limited, so we don't waste resources on envelopes the transport will drop ([#5813](https://github.com/getsentry/sentry-java/pull/5813))
+ - These skipped replays are now reported as `ratelimit_backoff` discarded events in client reports, so they no longer disappear from drop statistics. One event is recorded per buffer flush rather than per segment.
+ - Buffer mode is also kept while rate-limited instead of switching to session mode, so the rolling buffer stays warm and the next error after the rate limit expires can send a complete replay.
+
+### Performance
+
+- Create the outbox and cache directories lazily in their consumers instead of during SDK init, moving the `mkdirs()` calls off the init (main) thread ([#5792](https://github.com/getsentry/sentry-java/pull/5792))
+- Reduce the number of SDK threads: `LifecycleWatcher` now schedules the session-end task on the shared timer executor instead of creating a dedicated `java.util.Timer` thread ([#5819](https://github.com/getsentry/sentry-java/pull/5819))
+- Reduce the number of SDK threads: `RateLimiter` now schedules its rate-limit-lifted notifications on the shared timer executor instead of creating a dedicated `java.util.Timer` thread ([#5814](https://github.com/getsentry/sentry-java/pull/5814))
+- Speed up deserialization of arbitrary JSON objects by typing numbers without throwing exceptions ([#5783](https://github.com/getsentry/sentry-java/pull/5783))
+
+### Dependencies
+
+- Bump Native SDK from v0.15.4 to v0.16.0 ([#5845](https://github.com/getsentry/sentry-java/pull/5845))
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0160)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.15.4...0.16.0)
+
+## 8.50.1
+
+### Fixes
+
+- Pin the published Sentry Android SDK's AAR metadata `minCompileSdk` to our `minSdk` (`21`) instead of AGP 9's new default of the SDK's own `compileSdk` (`37`), so apps that depend on the SDK aren't forced to raise their `compileSdk` ([#5823](https://github.com/getsentry/sentry-java/pull/5823))
+
+## 8.50.0
+
+### Android 17 support
+
+- We've put Android 17 through a set of rigorous tests. We're now officially giving it the Sentry stamp of compatibility .([#5796](https://github.com/getsentry/sentry-java/pull/5796))
+
+### Fixes
+
+- Reduce main-thread work during `Sentry.init` by resolving the shake-detector accelerometer off the main thread (~1.75ms on a Pixel 10) ([#5784](https://github.com/getsentry/sentry-java/pull/5784))
+- Backfill release, environment, distribution, tags, and app version/build—and use the matching replay-on-error sample rate—for `ApplicationExitInfo` ANR and native crash events captured before SDK initialization, without reusing options cached by a later app update ([#5762](https://github.com/getsentry/sentry-java/pull/5762))
+- `SentryTagModifierNode.isImportantForBounds` now matches the default behavior and returns `true` ([#5789](https://github.com/getsentry/sentry-java/pull/5789))
+- Prevent a `StackOverflowError` when a `beforeSend`, `beforeBreadcrumb`, `beforeSendLog`, or `beforeEnvelope` callback triggers another capture (directly or through a logging integration such as Timber) ([#5737](https://github.com/getsentry/sentry-java/pull/5737))
+ - Captures made from within a user callback (event, transaction, breadcrumb, log, envelope, or check-in) are now dropped while that callback runs, instead of recursing. Captures made by event processors are unaffected.
+- Replace deprecated `ThrowableProxy` with `LogEvent#getThrown()` in `sentry-log4j2` ([#5751](https://github.com/getsentry/sentry-java/pull/5751))
+
+### Dependencies
+
+- Bump Native SDK from v0.15.3 to v0.15.4 ([#5793](https://github.com/getsentry/sentry-java/pull/5793))
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0154)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.15.3...0.15.4)
+- The SDK is now compiled with Android Gradle Plugin 9.2.1 ([#5779](https://github.com/getsentry/sentry-java/pull/5779))
+
+## 8.49.0
+
+### Features
+
+- Session Replay: Record segment names (transaction names) ([#5763](https://github.com/getsentry/sentry-java/pull/5763))
+
+- Add `io.sentry:sentry-opentelemetry-bom` to align Sentry OpenTelemetry modules with tested OpenTelemetry dependencies ([#5629](https://github.com/getsentry/sentry-java/pull/5629))
+ - Spring Boot Gradle plugin: add the Sentry BOM to `dependencyManagement`; explicit imports are applied after Spring Boot's implicit BOM
+ ```kotlin
+ dependencyManagement {
+ imports {
+ mavenBom("io.sentry:sentry-opentelemetry-bom:")
+ }
+ }
+ ```
+ - Gradle: import it as a platform and omit versions from Sentry OpenTelemetry and OpenTelemetry dependencies
+ ```kotlin
+ implementation(platform("io.sentry:sentry-opentelemetry-bom:"))
+ ```
+ - Maven: import it before Spring Boot's BOM in the same `` block, or in the child POM when using `spring-boot-starter-parent`
+ ```xml
+
+ io.sentry
+ sentry-opentelemetry-bom
+ ${sentry.version}
+ pom
+ import
+
+ ```
+
+### Fixes
+
+- Session Replay: Fix first recording segment missing for replays in `buffer` mode ([#5753](https://github.com/getsentry/sentry-java/pull/5753))
+- Session Replay: Fix error-to-replay linkage in `buffer` mode ([#5754](https://github.com/getsentry/sentry-java/pull/5754))
+- Prevent logs and metrics from remaining queued after a flush scheduling race ([#5756](https://github.com/getsentry/sentry-java/pull/5756))
+- Fix main thread identification for tombstone (native crash) events ([#5742](https://github.com/getsentry/sentry-java/pull/5742))
+- Prevent malformed JDBC URLs, which may contain credentials, from being printed to stdout ([#5656](https://github.com/getsentry/sentry-java/pull/5656))
+- Restrict JVM-global proxy authentication credentials to challenges from the configured proxy host ([#5656](https://github.com/getsentry/sentry-java/pull/5656))
+- Sanitize Spring 7 and Spring Jakarta WebClient span descriptions to prevent embedded URL credentials from being sent to Sentry ([#5656](https://github.com/getsentry/sentry-java/pull/5656))
+- Respect `tracePropagationTargets` when injecting Sentry tracing headers through the OpenTelemetry OTLP propagator ([#5656](https://github.com/getsentry/sentry-java/pull/5656))
+
+### Performance
+
+- Schedule transaction idle/deadline timeouts on a shared, dedicated executor instead of spawning a `Timer` thread per transaction ([#5670](https://github.com/getsentry/sentry-java/pull/5670))
+
+### Dependencies
+
+- Bump OpenTelemetry to support Spring Boot 4.1 ([#5573](https://github.com/getsentry/sentry-java/pull/5573))
+ - If this causes issues for you because you are also using Spring Boot Dependency Management Plugin (io.spring.dependency-management),
+ which may downgrade the OpenTelemetry SDK, please have a look at the changelog entry above that explains how to use `sentry-opentelemetry-bom`.
+ - OpenTelemetry to 1.63.0 (was 1.60.1)
+ - OpenTelemetry Instrumentation to 2.29.0 (was 2.26.0)
+ - OpenTelemetry Instrumentation Alpha to 2.29.0-alpha (was 2.26.0-alpha)
+ - OpenTelemetry Semantic Conventions to 1.42.0 (was 1.40.0)
+ - OpenTelemetry Semantic Conventions Alpha to 1.42.0-alpha (was 1.40.0-alpha)
+- Bump Native SDK from v0.15.2 to v0.15.3 ([#5728](https://github.com/getsentry/sentry-java/pull/5728))
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0153)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.15.2...0.15.3)
+
+## 8.48.0
+
+### Features
+
+- 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))
+ - Requires standalone app start tracing (`options.isEnableStandaloneAppStartTracing`). Call `extendAppStart()` in `Application.onCreate` after SDK init and `finishExtendedAppStart()` when done:
+
+ ```kotlin
+ Sentry.extendAppStart()
+
+ // Optionally, retrieve the extended app start span to attach your own child spans
+ val child = Sentry.getExtendedAppStartSpan()?.startChild("preload", "Preload resources")
+ // ... extra launch-time work ...
+ child?.finish()
+
+ Sentry.finishExtendedAppStart()
+ ```
+- 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))
+- Expose sentry-native's heartbeat-based app-hang detection through `SentryAndroidOptions` ([#5623](https://github.com/getsentry/sentry-java/pull/5623))
+ - Enable via `setEnableNdkAppHangTracking(true)` (disabled by default) and tune the timeout with `setNdkAppHangTimeoutIntervalMillis(...)` (default `5000` ms), or the `io.sentry.ndk.app-hang.enable` / `io.sentry.ndk.app-hang.timeout-interval-millis` manifest entries
+ - Intended for hybrid SDKs: emit the heartbeat by calling the native `sentry_app_hang_heartbeat()` from the thread you want monitored. Independent of the JVM-based ANR detection (`setAnrEnabled`)
+- Support the `io.sentry.tombstone.report-historical` manifest option to enable historical tombstone reporting via `AndroidManifest.xml` `` ([#5683](https://github.com/getsentry/sentry-java/pull/5683))
+
+### Fixes
+
+- Fix `NoSuchMethodError` from using `Math.floorDiv`/`Math.floorMod` overloads that are unavailable on Java 8 ([#5743](https://github.com/getsentry/sentry-java/pull/5743))
+- Fix main thread identification parsing for ApplicationExitInfo ANRs ([#5733](https://github.com/getsentry/sentry-java/pull/5733))
+- Do not send threads without stacktraces for ApplicationExitInfo ANRs ([#5733](https://github.com/getsentry/sentry-java/pull/5733))
+- Record byte-level client reports when event processors discard logs or trace metrics ([#5718](https://github.com/getsentry/sentry-java/pull/5718))
+- Name the device-info caching thread `SentryDeviceInfoCache` so all threads spawned by the SDK are identifiable ([#5684](https://github.com/getsentry/sentry-java/pull/5684))
+- Apply byte-category rate limits to log and trace metric envelope items ([#5716](https://github.com/getsentry/sentry-java/pull/5716))
+
+### Performance
+
+- Skip `Hint` allocation in `Scope.addBreadcrumb` when no `beforeBreadcrumb` callback is set ([#5689](https://github.com/getsentry/sentry-java/pull/5689))
+- Speed up scope persistence by detecting the Sentry executor thread via a marker instead of a `Thread.getName()` name scan on every scope mutation ([#5691](https://github.com/getsentry/sentry-java/pull/5691))
+- Remove executor prewarm during SDK init ([#5681](https://github.com/getsentry/sentry-java/pull/5681))
+ - The single-threaded `SentryExecutorService` queued the prewarm work ahead of the first useful task, so it could only delay init work, never speed it up; the thread and class loading it warmed are paid identically by the first real task submitted right after.
+
+### Dependencies
+
+- Bump Native SDK from v0.15.2 to v0.15.3 ([#5623](https://github.com/getsentry/sentry-java/pull/5623))
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0153)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.15.2...0.15.3)
+
+## 8.47.0
+
+### Behavioral Changes
+
+- `SentryOkHttpInterceptor::intercept` now throws `IOException`. This is a source-only and Java-only breaking change ([#5654](https://github.com/getsentry/sentry-java/pull/5654))
+
+### Fixes
+
+- Fix fragment tracing not working with detach/attach navigation ([#5660](https://github.com/getsentry/sentry-java/pull/5660))
+- Don't start a redundant UI interaction transaction when a transaction is already bound to the Scope ([#5491](https://github.com/getsentry/sentry-java/issues/5491))
+ - Previously, `SentryGestureListener` always started a UI transaction and only afterwards skipped binding it to the Scope when a manually-bound transaction already existed, leaving the new transaction to be dropped as an idle transaction without children.
+- Fix potential NPE within `Scope.endSession()` ([#5657](https://github.com/getsentry/sentry-java/pull/5657))
+- Fix memory leak in `ReplayIntegration` due to persisting executor not being shut down ([#5627](https://github.com/getsentry/sentry-java/pull/5627))
+- Fix AbstractMethodError when compose-ui 1.11+ is used in combination with `Modifier.sentryTag()` or the Sentry Kotlin compiler plugin ([#5672](https://github.com/getsentry/sentry-java/pull/5672))
+
+### Performance
+
+- Speed up touch gesture target detection on deeply nested view hierarchies by hit-testing in local coordinates instead of calling `getLocationOnScreen` per view ([#5595](https://github.com/getsentry/sentry-java/pull/5595))
+- Probe class availability without initializing the class during SDK init ([#5635](https://github.com/getsentry/sentry-java/pull/5635))
+- Avoid constructing an exception per view when resolving view ids during view-hierarchy and gesture capture ([#5631](https://github.com/getsentry/sentry-java/pull/5631))
+- Start the frame metrics thread lazily on first collection instead of during SDK init ([#5641](https://github.com/getsentry/sentry-java/pull/5641))
+- Reduce `SentryId` and `SpanId` allocation overhead by replacing their per-instance `LazyEvaluator` (and its lock) with a lightweight lazily-generated `String`. ([#5645](https://github.com/getsentry/sentry-java/pull/5645))
+- Lazily allocate the `ReentrantLock` backing `AutoClosableReentrantLock` to avoid eager lock allocations for SDK objects that never contend during `SentryAndroid.init` ([#5643](https://github.com/getsentry/sentry-java/pull/5643))
+
+## 8.46.0
+
+### Fixes
+
+- Session Replay: Fix network detail response body size being unknown for gzip-compressed responses ([#5592](https://github.com/getsentry/sentry-java/pull/5592))
+
+### Behavioral Changes
+
+- Collections returned by scope (e.g. `getBreadcrumbs`, `getTags`, `getAttachments`) are shared state and should not be mutated. ([#5541](https://github.com/getsentry/sentry-java/pull/5541))
+ - Previously, when going through `CombinedScopeView`, we were returning a copy where mutations didn't show up in the underlying scopes.
+ - This has now changed in order to reduce SDK overhead.
+- `Date` objects returned by SDK data model getters are shared state and should not be mutated. ([#5603](https://github.com/getsentry/sentry-java/pull/5603))
+ - Previously, these getters returned defensive copies for some date fields.
+ - This has now changed in order to reduce SDK overhead.
+
+### Performance
+
+- Reduce writer buffer size from 8192 to 512 ([#5544](https://github.com/getsentry/sentry-java/pull/5544))
+- Remove redundant event map copies ([#5536](https://github.com/getsentry/sentry-java/pull/5536))
+- Optimize combined scope by adding an early return if only one scope has data ([#5541](https://github.com/getsentry/sentry-java/pull/5541))
+- Reduce model access overhead by avoiding defensive `Date` copies in SDK data model getters. ([#5603](https://github.com/getsentry/sentry-java/pull/5603))
+- Reduce timestamp parsing and formatting overhead with Sentry-specific ISO-8601 handling. ([#5602](https://github.com/getsentry/sentry-java/pull/5602))
+- Reduce JSON serialization overhead by creating the reflection serializer only when unknown-object fallback serialization is needed. ([#5601](https://github.com/getsentry/sentry-java/pull/5601))
+- Reduce JSON serialization overhead by allocating reflection cycle-tracking state only when reflection serialization is used. ([#5600](https://github.com/getsentry/sentry-java/pull/5600))
+- Reduce context serialization overhead by sorting key snapshots with arrays instead of temporary lists. ([#5599](https://github.com/getsentry/sentry-java/pull/5599))
+- Reduce breadcrumb allocation overhead by creating the `Breadcrumb` data map only when data is added. ([#5598](https://github.com/getsentry/sentry-java/pull/5598))
+- Reduce JSON serialization overhead by lowering the initial `JsonWriter` nesting stack size while preserving on-demand growth. ([#5591](https://github.com/getsentry/sentry-java/pull/5591))
+- Reduce timestamp helper overhead by replacing unnecessary `Calendar` usage in `DateUtils` with direct `Date` creation. ([#5589](https://github.com/getsentry/sentry-java/pull/5589))
+- Reduce Android startup overhead by using the default timezone directly on older devices or when no timezone info is available in the locale. ([#5587](https://github.com/getsentry/sentry-java/pull/5587))
+
+## 8.45.0
+
+### Features
+
+- On Android 15+ (API 35), the standalone `app.start` transaction now reports why the OS started the process via `app.vitals.start.reason` trace data (e.g. `launcher`, `broadcast`, `service`, `content_provider`), derived from `ApplicationStartInfo.getReason()`. You can search and group by this attribute in the Trace Explorer. ([#5552](https://github.com/getsentry/sentry-java/pull/5552))
+
+### Fixes
+
+- Use `System.nanoTime()` for cron check-in duration measurement to avoid incorrect durations from wall-clock adjustments ([#5611](https://github.com/getsentry/sentry-java/pull/5611))
+- Fix crash when `getHistoricalProcessStartReasons` is called from an isolated or wrong-userId process ([#5597](https://github.com/getsentry/sentry-java/pull/5597))
+- Release `MediaMuxer` when a replay segment has no encodable frames to avoid a resource leak ([#5583](https://github.com/getsentry/sentry-java/pull/5583))
+
+### Dependencies
+
+- Bump Native SDK from v0.15.1 to v0.15.2 ([#5610](https://github.com/getsentry/sentry-java/pull/5610))
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0152)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.15.1...0.15.2)
+
+## 8.44.1
+
+### Fixes
+
+- Fix `FirstDrawDoneListener` leaking an `OnGlobalLayoutListener` per registration ([#5567](https://github.com/getsentry/sentry-java/pull/5567))
+
+### Features
+
+- Add experimental `SentrySQLiteDriver` to `sentry-android-sqlite` for instrumenting `androidx.sqlite.SQLiteDriver` ([#5563](https://github.com/getsentry/sentry-java/pull/5563))
+ - To use it, pass `SQLiteDriver` to `SentrySQLiteDriver.create(...)`
+ - Requires `androidx.sqlite:sqlite` (2.5.0+) on runtime classpath (typically provided by Room or SQLDelight)
+
+### Dependencies
+
+- Bump Native SDK from v0.15.0 to v0.15.1 ([#5570](https://github.com/getsentry/sentry-java/pull/5570))
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0151)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.15.0...0.15.1)
+
+## 8.44.0
+
+### Features
+
+- 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)
+
+### Improvements
+
+- Reduce boxing to improve performance ([#5523](https://github.com/getsentry/sentry-java/pull/5523), [#5527](https://github.com/getsentry/sentry-java/pull/5527), [#5551](https://github.com/getsentry/sentry-java/pull/5551))
+- Replace `Date` with a unix timestamp in `SentryNanotimeDate` to improve performance ([#5550](https://github.com/getsentry/sentry-java/pull/5550))
+ - `SentryNanotimeDate` is now marked `@ApiStatus.Internal`. A new `(long unixDateMillis, long nanos)` constructor was added, where `unixDateMillis` is milliseconds since the epoch. The existing `(Date, long)` constructor is retained but deprecated.
+
+### Dependencies
+
+- Upgrade to asyncProfiler 4.4 ([#5418](https://github.com/getsentry/sentry-java/pull/5418))
+- Bump Native SDK from v0.14.2 to v0.15.0 ([#5528](https://github.com/getsentry/sentry-java/pull/5528))
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0150)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.14.2...0.15.0)
+
+### Fixes
+
+- Fix attachments being duplicated on native events that carry scope attachments ([#5548](https://github.com/getsentry/sentry-java/pull/5548))
+- Fix performance collector scheduling many tasks in a row ([#5524](https://github.com/getsentry/sentry-java/pull/5524))
+
+## 8.43.3
+
+### Fixes
+
+- Fix crash when `getHistoricalProcessStartReasons` is called from an isolated or wrong-userId process ([#5597](https://github.com/getsentry/sentry-java/pull/5597))
+
+## 8.43.2
+
+### Improvements
+
+- Improve SDK init performance by replacing `java.net.URI` with custom string parsing for DSN ([#5448](https://github.com/getsentry/sentry-java/pull/5448))
+- Remove unnecessary boxing to improve performance ([#5520](https://github.com/getsentry/sentry-java/pull/5520))
+
+### Fixes
+
+- Session Replay: Fix `VerifyError` in Compose masking under DexGuard/R8 obfuscation ([#5507](https://github.com/getsentry/sentry-java/pull/5507))
+- Session Replay: Fix Compose view masking not working on obfuscated/minified builds ([#5503](https://github.com/getsentry/sentry-java/pull/5503))
+
+## 8.43.1
+
+### Fixes
+
+- Session Replay: Fix replay recording freezing on screens with continuous animations ([#5489](https://github.com/getsentry/sentry-java/pull/5489))
+- Session Replay: Populate `trace_ids` in replay events to enable searching replays by trace ID ([#5473](https://github.com/getsentry/sentry-java/pull/5473))
+
+## 8.43.0
+
+### Features
+
+- Session Replay: Add `ReplayFrameObserver` for observing captured replay frames ([#5386](https://github.com/getsentry/sentry-java/pull/5386))
+
+ ```kotlin
+ SentryAndroid.init(context) { options ->
+ options.sessionReplay.frameObserver =
+ SentryReplayOptions.ReplayFrameObserver { hint, frameTimestamp, screenName ->
+ val bitmap = hint.getAs(TypeCheckHint.REPLAY_FRAME_BITMAP, Bitmap::class.java)
+ if (bitmap != null) {
+ try {
+ // Process the masked replay frame
+ myAnalyzer.processFrame(bitmap, frameTimestamp, screenName)
+ } finally {
+ bitmap.recycle()
+ }
+ }
+ }
+ }
+ ```
+- Parse ART memory and garbage collector info from ANR tombstones into ART context ([#5428](https://github.com/getsentry/sentry-java/pull/5428))
+
+## 8.42.0
+
+### Features
+
+- Add option to attach raw tombstone protobuf on native crash events ([#5446](https://github.com/getsentry/sentry-java/pull/5446))
+ - Enable via `options.isAttachRawTombstone = true` or manifest: ` `
+- Add API to clear feature flags from scopes ([#5426](https://github.com/getsentry/sentry-java/pull/5426))
+- Add support to configure reporting historical ANRs via `AndroidManifest.xml` using the `io.sentry.anr.report-historical` attribute ([#5387](https://github.com/getsentry/sentry-java/pull/5387))
+
+### Dependencies
+
+- Bump Gradle from v9.5.0 to v9.5.1 ([#5419](https://github.com/getsentry/sentry-java/pull/5419))
+ - [changelog](https://github.com/gradle/gradle/blob/master/CHANGELOG.md#v951)
+ - [diff](https://github.com/gradle/gradle/compare/v9.5.0...v9.5.1)
+- Bump Native SDK from v0.14.0 to v0.14.2 ([#5433](https://github.com/getsentry/sentry-java/pull/5433), [#5441](https://github.com/getsentry/sentry-java/pull/5441))
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0142)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.14.0...0.14.2)
+- Bump SAGP (Sentry Android Gradle Plugin) from v6.0.0-alpha.6 to v6.6.0 ([#5427](https://github.com/getsentry/sentry-java/pull/5427))
+ - [changelog](https://github.com/getsentry/sentry-android-gradle-plugin/blob/main/CHANGELOG.md)
+ - [diff](https://github.com/getsentry/sentry-android-gradle-plugin/compare/6.0.0-alpha.6...6.6.0)
+
+## 8.41.0
+
+### Features
+
+- Session Replay: experimental support for capturing `SurfaceView` content (e.g. Unity, video players, maps) ([#5333](https://github.com/getsentry/sentry-java/pull/5333))
+ - To enable, set `options.sessionReplay.isCaptureSurfaceViews = true`
+ - Or via manifest: ` `
+ - **Warning:** masking granularity is at the SurfaceView level only — the SDK cannot mask individual elements rendered inside the SurfaceView (e.g. native Unity UI, map labels, video frames). Only enable for SurfaceViews whose content is safe to record.
+- Add `Sentry.feedback()` API for `show()` and `capture()` ([#5349](https://github.com/getsentry/sentry-java/pull/5349))
+ - `Sentry.showUserFeedbackDialog()` is deprecated in favor of `Sentry.feedback().show()`
+ - `Sentry.captureFeedback()` is deprecated in favor of `Sentry.feedback().capture()`
+ - `Sentry.captureUserFeedback()` and `UserFeedback` are deprecated in favor of `Sentry.feedback().capture()` with the new `Feedback` type
+ - `SentryUserFeedbackDialog` is deprecated in favor of `SentryUserFeedbackForm`
+ - All deprecated APIs will be removed in the next major version
+- Deprecate `SentryUserFeedbackButton` (View-based and Compose-based) ([#5350](https://github.com/getsentry/sentry-java/pull/5350))
+ - It will be removed in the next major version
+- Add per-form shake-to-show support for `SentryUserFeedbackForm` ([#5353](https://github.com/getsentry/sentry-java/pull/5353))
+ - Useful for enabling shake-to-report on specific screens instead of globally
+ ```kotlin
+ SentryUserFeedbackForm.Builder(activity)
+ .configurator { it.isUseShakeGesture = true }
+ .create()
+ ```
+- Add support for Kafka ([#5249](https://github.com/getsentry/sentry-java/pull/5249))
+ - You will need to add the `sentry-kafka` dependency and opt-in via the new option.
+ - Set `options.setEnableQueueTracing(true)` on `Sentry.init`
+ - Or set `sentry.enable-queue-tracing=true` in `application.properties`
+ - For Spring Boot Kafka is auto instrumented and no further configuration is needed.
+ - also see https://docs.sentry.io/platforms/java/guides/spring-boot/integrations/kafka/
+ - When using `kafka-clients` directly
+ - you need to wrap your `KafkaProducer` via `SentryKafkaProducer.wrap(kafkaProducer)` to get `queue.publish` spans
+ - and you may use our `SentryKafkaConsumerTracing.withTracing` helper to instrument the consumer side manually.
+ - also see https://docs.sentry.io/platforms/java/integrations/kafka/
+
+### Fixes
+
+- Fix soft input keyboard not being shown on the Feedback form ([#5359](https://github.com/getsentry/sentry-java/pull/5359))
+- Fix shake-to-report not triggering on some devices due to high acceleration threshold ([#5366](https://github.com/getsentry/sentry-java/pull/5366))
+- Fix feedback form retaining previous message when shown again via shake ([#5366](https://github.com/getsentry/sentry-java/pull/5366))
+- Avoid stack overflow when deserializing large flat JSON objects ([#5361](https://github.com/getsentry/sentry-java/pull/5361))
+
+### Dependencies
+
+- Bump Native SDK from v0.13.7 to v0.14.0 ([#5334](https://github.com/getsentry/sentry-java/pull/5334), [#5365](https://github.com/getsentry/sentry-java/pull/5365))
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0140)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.13.7...0.14.0)
+- Bump Gradle from v9.4.1 to v9.5.0 ([#5344](https://github.com/getsentry/sentry-java/pull/5344))
+ - [changelog](https://github.com/gradle/gradle/blob/master/CHANGELOG.md#v950)
+ - [diff](https://github.com/gradle/gradle/compare/v9.4.1...v9.5.0)
+
+## 8.40.0
+
+### Fixes
+
+- Fix `NoSuchMethodError` for `LayoutCoordinates.localBoundingBoxOf$default` on Compose touch dispatch with AGP 8.13 and `minSdk < 24` ([#5302](https://github.com/getsentry/sentry-java/pull/5302))
+- Fix reporting OkHttp's synthetic 504 "Unsatisfiable Request" responses as errors for `CacheControl.FORCE_CACHE` cache misses ([#5299](https://github.com/getsentry/sentry-java/pull/5299))
+- Make `SentryGestureDetector` thread-safe and recycle `VelocityTracker` per gesture ([#5301](https://github.com/getsentry/sentry-java/pull/5301))
+- Fix duplicate `ui.click` breadcrumbs when another `Window.Callback` wraps `SentryWindowCallback` ([#5300](https://github.com/getsentry/sentry-java/pull/5300))
+
+### Dependencies
+
+- Bump Native SDK from v0.13.6 to v0.13.7 ([#5296](https://github.com/getsentry/sentry-java/pull/5296))
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0137)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.13.6...0.13.7)
+
+## 8.39.1
+
+### Fixes
+
+- Fix `JsonObjectReader` and `MapObjectReader` hanging indefinitely when deserialization errors leave the reader in an inconsistent state ([#5293](https://github.com/getsentry/sentry-java/pull/5293))
+ - Failed collection values are now skipped so parsing can continue
+ - Skipped collection values emit `WARNING` logs
+ - Unknown-key failures and unrecoverable recovery failures emit `ERROR` logs
+
+## 8.39.0
+
+### Fixes
+
+- Fix ANR caused by `GestureDetectorCompat` Handler/MessageQueue lock contention in `SentryWindowCallback` ([#5138](https://github.com/getsentry/sentry-java/pull/5138))
+
+### Internal
+
+- Bump AGP version from v8.6.0 to v8.13.1 ([#5063](https://github.com/getsentry/sentry-java/pull/5063))
+
+### Dependencies
+
+- Bump Native SDK from v0.13.3 to v0.13.6 ([#5277](https://github.com/getsentry/sentry-java/pull/5277))
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0136)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.13.3...0.13.6)
+- Bump Gradle from v8.14.3 to v9.4.1 ([#5063](https://github.com/getsentry/sentry-java/pull/5063))
+ - [changelog](https://github.com/gradle/gradle/blob/master/CHANGELOG.md#v941)
+ - [diff](https://github.com/gradle/gradle/compare/v8.14.3...v9.4.1)
+
+## 8.38.0
+
+### Features
+
+- Prevent cross-organization trace continuation ([#5136](https://github.com/getsentry/sentry-java/pull/5136))
+ - By default, the SDK now extracts the organization ID from the DSN (e.g. `o123.ingest.sentry.io`) and compares it with the `sentry-org_id` value in incoming baggage headers. When the two differ, the SDK starts a fresh trace instead of continuing the foreign one. This guards against accidentally linking traces across organizations.
+ - New option `enableStrictTraceContinuation` (default `false`): when enabled, both the SDK's org ID **and** the incoming baggage org ID must be present and match for a trace to be continued. Traces with a missing org ID on either side are rejected. Configurable via code (`setStrictTraceContinuation(true)`), `sentry.properties` (`enable-strict-trace-continuation=true`), Android manifest (`io.sentry.strict-trace-continuation.enabled`), or Spring Boot (`sentry.strict-trace-continuation=true`).
+ - New option `orgId`: allows explicitly setting the organization ID for self-hosted and Relay setups where it cannot be extracted from the DSN. Configurable via code (`setOrgId("123")`), `sentry.properties` (`org-id=123`), Android manifest (`io.sentry.org-id`), or Spring Boot (`sentry.org-id=123`).
+- Android: Attachments on the scope will now be synced to native ([#5211](https://github.com/getsentry/sentry-java/pull/5211))
+- Add THIRD_PARTY_NOTICES.md for vendored third-party code, bundled as SENTRY_THIRD_PARTY_NOTICES.md in the sentry JAR under META-INF ([#5186](https://github.com/getsentry/sentry-java/pull/5186))
+
+### Improvements
+
+- Do not retrieve `ActivityManager` if API < 35 on SDK init ([#5275](https://github.com/getsentry/sentry-java/pull/5275))
+
+## 8.37.1
+
+### Fixes
+
+- Fix deadlock in `SentryContextStorage.root()` with virtual threads and OpenTelemetry agent ([#5234](https://github.com/getsentry/sentry-java/pull/5234))
+
+## 8.37.0
+
+### Fixes
+
+- Session Replay: Fix Compose text masking mismatch with weighted text ([#5218](https://github.com/getsentry/sentry-java/pull/5218))
+
+### Features
+
+- Add cache tracing instrumentation for Spring Boot 2, 3, and 4 ([#5165](https://github.com/getsentry/sentry-java/pull/5165))
+ - Wraps Spring `CacheManager` and `Cache` beans to produce cache spans
+ - Set `sentry.enable-cache-tracing` to `true` to enable this feature
+- Add JCache (JSR-107) cache tracing via new `sentry-jcache` module ([#5165](https://github.com/getsentry/sentry-java/pull/5165))
+ - Wraps JCache `Cache` with `SentryJCacheWrapper` to produce cache spans
+ - Set the `enableCacheTracing` option to `true` to enable this feature
+- Add configurable `IScopesStorageFactory` to `SentryOptions` for providing a custom `IScopesStorage`, e.g. when the default `ThreadLocal`-backed storage is incompatible with non-pinning thread models ([#5199](https://github.com/getsentry/sentry-java/pull/5199))
+- Android: Add `beforeErrorSampling` callback to Session Replay ([#5214](https://github.com/getsentry/sentry-java/pull/5214))
+ - Allows filtering which errors trigger replay capture before the `onErrorSampleRate` is checked
+ - Returning `false` skips replay capture entirely for that error; returning `true` proceeds with the normal sample rate check
+ - Example usage:
+ ```kotlin
+ SentryAndroid.init(context) { options ->
+ options.sessionReplay.beforeErrorSampling =
+ SentryReplayOptions.BeforeErrorSamplingCallback { event, hint ->
+ // Only capture replay for crashes (excluding e.g. handled exceptions)
+ event.isCrashed
+ }
+ }
+ ```
+
+### Dependencies
+
+- Bump Native SDK from v0.13.2 to v0.13.3 ([#5215](https://github.com/getsentry/sentry-java/pull/5215))
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0133)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.13.2...0.13.3)
+- Bump OpenTelemetry ([#5225](https://github.com/getsentry/sentry-java/pull/5225))
+ - `opentelemetry` to `1.60.1` (was `1.57.0`)
+ - `opentelemetry-instrumentation` to `2.26.0` (was `2.23.0`)
+ - `opentelemetry-instrumentation-alpha` to `2.26.0-alpha` (was `2.23.0-alpha`)
+ - `opentelemetry-semconv` to `1.40.0` (was `1.37.0`)
+ - `opentelemetry-semconv-alpha` to `1.40.0-alpha` (was `1.37.0-alpha`)
+
+## 8.36.0
+
+### Features
+
+- Show feedback form on device shake ([#5150](https://github.com/getsentry/sentry-java/pull/5150))
+ - Enable via `options.getFeedbackOptions().setUseShakeGesture(true)` or manifest meta-data `io.sentry.feedback.use-shake-gesture`
+ - Uses the device's accelerometer — no special permissions required
+
+### Fixes
+
+- Support masking/unmasking and click/scroll detection for Jetpack Compose 1.10+ ([#5189](https://github.com/getsentry/sentry-java/pull/5189))
+
+### Dependencies
+
+- Bump Native SDK from v0.13.1 to v0.13.2 ([#5181](https://github.com/getsentry/sentry-java/pull/5181))
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0132)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.13.1...0.13.2)
+- Bump `com.abovevacant:epitaph` to `0.1.1` to avoid old D8/R8 dexing crashes in downstream Android builds on old AGP versions such as 7.4.x. ([#5200](https://github.com/getsentry/sentry-java/pull/5200))
+ - [changelog](https://github.com/abovevacant/epitaph/blob/main/CHANGELOG.md#011---2026-03-16)
+ - [diff](https://github.com/abovevacant/epitaph/compare/v0.1.0...v0.1.1)
+
+## 8.35.0
+
+### Fixes
+
+- Android: Remove the dependency on protobuf-lite for tombstones ([#5157](https://github.com/getsentry/sentry-java/pull/5157))
+
+### Features
+
+- Add new experimental option to capture profiles for ANRs ([#4899](https://github.com/getsentry/sentry-java/pull/4899))
+ - This feature will capture a stack profile of the main thread when it gets unresponsive
+ - The profile gets attached to the ANR event on the next app start, providing a flamegraph of the ANR issue on the sentry issue details page
+ - Enable via `options.setAnrProfilingSampleRate()` or AndroidManifest.xml: ` `
+ - The sample rate controls the probability of collecting a profile for each detected foreground ANR (0.0 to 1.0, null to disable)
+
+### Behavioral Changes
+
+- Add `enableAnrFingerprinting` option which assigns static fingerprints to ANR events with system-only stacktraces
+ - When enabled, ANRs whose stacktraces contain only system frames (e.g. `java.lang` or `android.os`) are grouped into a single issue instead of creating many separate issues
+ - This will help to reduce overall ANR issue noise in the Sentry dashboard
+ - **IMPORTANT:** This option is enabled by default.
+ - Disable via `options.setEnableAnrFingerprinting(false)` or AndroidManifest.xml: ` `
+
+## 8.34.1
+
+### Fixes
+
+- Common: Finalize previous session even when auto session tracking is disabled ([#5154](https://github.com/getsentry/sentry-java/pull/5154))
+- Android: Add `filterTouchesWhenObscured` to prevent Tapjacking on user feedback dialog ([#5155](https://github.com/getsentry/sentry-java/pull/5155))
+- Android: Add proguard rules to prevent error about missing Replay classes ([#5153](https://github.com/getsentry/sentry-java/pull/5153))
+
+## 8.34.0
+
+### Features
+
+- Allow configuring shutdown and session flush timeouts externally ([#4641](https://github.com/getsentry/sentry-java/pull/4641))
+ - `sentry.properties`: `shutdown-timeout-millis`, `session-flush-timeout-millis`
+ - Environment variables: `SENTRY_SHUTDOWN_TIMEOUT_MILLIS`, `SENTRY_SESSION_FLUSH_TIMEOUT_MILLIS`
+ - Spring Boot `application.properties`: `sentry.shutdownTimeoutMillis`, `sentry.sessionFlushTimeoutMillis`
+- Add scope-level attributes API ([#5118](https://github.com/getsentry/sentry-java/pull/5118)) via ([#5148](https://github.com/getsentry/sentry-java/pull/5148))
+ - Automatically include scope attributes in logs and metrics ([#5120](https://github.com/getsentry/sentry-java/pull/5120))
+ - New APIs are `Sentry.setAttribute`, `Sentry.setAttributes`, `Sentry.removeAttribute`
+- Support collections and arrays in attribute type inference ([#5124](https://github.com/getsentry/sentry-java/pull/5124))
+- Add support for `SENTRY_SAMPLE_RATE` environment variable / `sample-rate` property ([#5112](https://github.com/getsentry/sentry-java/pull/5112))
+- Create `sentry-opentelemetry-otlp` and `sentry-opentelemetry-otlp-spring` modules for combining OpenTelemetry SDK OTLP export with Sentry SDK ([#5100](https://github.com/getsentry/sentry-java/pull/5100))
+ - OpenTelemetry is configured to send spans to Sentry directly using an OTLP endpoint.
+ - Sentry only uses trace and span ID from OpenTelemetry (via `OpenTelemetryOtlpEventProcessor`) but will not send spans through OpenTelemetry nor use OpenTelemetry `Context` for `Scopes` propagation.
+ - See the OTLP setup docs for [Java](https://docs.sentry.io/platforms/java/opentelemetry/setup/otlp/) and [Spring Boot](https://docs.sentry.io/platforms/java/guides/spring-boot/opentelemetry/setup/otlp/) for installation and configuration instructions.
+- Add screenshot masking support using view hierarchy ([#5077](https://github.com/getsentry/sentry-java/pull/5077))
+ - Masks sensitive content (text, images) in error screenshots using the same view hierarchy approach as Session Replay
+ - Requires the `sentry-android-replay` module to be present at runtime for masking to work
+ - Enable via code:
+ ```kotlin
+ SentryAndroid.init(context) { options ->
+ options.isAttachScreenshot = true
+ options.screenshot.setMaskAllText(true)
+ options.screenshot.setMaskAllImages(true)
+ // Or mask specific view classes
+ options.screenshot.addMaskViewClass("com.example.MyCustomView")
+ }
+ ```
+ - Or via `AndroidManifest.xml`:
+ ```xml
+
+
+
+ ```
+- The `ManifestMetaDataReader` now read the `DIST` ([#5107](https://github.com/getsentry/sentry-java/pull/5107))
+
+### Fixes
+
+- Fix attribute type detection for `Long`, `Short`, `Byte`, `BigInteger`, `AtomicInteger`, and `AtomicLong` being incorrectly inferred as `double` instead of `integer` ([#5122](https://github.com/getsentry/sentry-java/pull/5122))
+- Remove `AndroidRuntimeManager` StrictMode relaxation to prevent ANRs during SDK init ([#5127](https://github.com/getsentry/sentry-java/pull/5127))
+ - **IMPORTANT:** StrictMode violations may appear again in debug builds. This is intentional to prevent ANRs in production releases.
+- Fix crash when unregistering `SystemEventsBroadcastReceiver` with try-catch block. ([#5106](https://github.com/getsentry/sentry-java/pull/5106))
+- Use `peekDecorView` instead of `getDecorView` in `SentryGestureListener` to avoid forcing view hierarchy construction ([#5134](https://github.com/getsentry/sentry-java/pull/5134))
+- Log an actionable error message when Relay returns HTTP 413 (Content Too Large) ([#5115](https://github.com/getsentry/sentry-java/pull/5115))
+ - Also switch the client report discard reason for all HTTP 4xx/5xx errors (except 429) from `network_error` to `send_error`
+- Trim DSN string before parsing to avoid `URISyntaxException` caused by trailing whitespace ([#5113](https://github.com/getsentry/sentry-java/pull/5113))
+- Reduce allocations and bytecode instructions during `Sentry.init` ([#5135](https://github.com/getsentry/sentry-java/pull/5135))
+
+### Dependencies
+
+- Bump Native SDK from v0.12.7 to v0.13.1 ([#5104](https://github.com/getsentry/sentry-java/pull/5104))
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0131)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.12.7...0.13.1)
+
+## 8.33.0
+
+### Features
+
+- Add `installGroupsOverride` parameter to Build Distribution SDK for programmatic filtering, with support for configuration via properties file using `io.sentry.distribution.install-groups-override` ([#5066](https://github.com/getsentry/sentry-java/pull/5066))
+
+### Fixes
+
+- When merging tombstones with Native SDK, use the tombstone message if the Native SDK didn't explicitly provide one. ([#5095](https://github.com/getsentry/sentry-java/pull/5095))
+- Fix thread leak caused by eager creation of `SentryExecutorService` in `SentryOptions` ([#5093](https://github.com/getsentry/sentry-java/pull/5093))
+ - There were cases where we created options that ended up unused but we failed to clean those up.
+- Attach user attributes to logs and metrics regardless of `sendDefaultPii` ([#5099](https://github.com/getsentry/sentry-java/pull/5099))
+- No longer log a warning if a logging integration cannot initialize Sentry due to missing DSN ([#5075](https://github.com/getsentry/sentry-java/pull/5075))
+ - While this may have been useful to some, it caused lots of confusion.
+- Session Replay: Add `androidx.camera.view.PreviewView` to default `maskedViewClasses` to mask camera previews by default. ([#5097](https://github.com/getsentry/sentry-java/pull/5097))
+
+### Dependencies
+
+- Bump Native SDK from v0.12.4 to v0.12.7 ([#5071](https://github.com/getsentry/sentry-java/pull/5071), [#5098](https://github.com/getsentry/sentry-java/pull/5098))
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0127)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.12.4...0.12.7)
+
+### Internal
+
+- Add integration to track session replay custom masking ([#5070](https://github.com/getsentry/sentry-java/pull/5070))
+
+## 8.32.0
+
+### Features
+
+- Add `installGroups` property to Build Distribution SDK ([#5062](https://github.com/getsentry/sentry-java/pull/5062))
+- Update Android targetSdk to API 36 (Android 16) ([#5016](https://github.com/getsentry/sentry-java/pull/5016))
+- Add AndroidManifest support for Spotlight configuration via `io.sentry.spotlight.enable` and `io.sentry.spotlight.url` ([#5064](https://github.com/getsentry/sentry-java/pull/5064))
+- Collect database transaction spans (`BEGIN`, `COMMIT`, `ROLLBACK`) ([#5072](https://github.com/getsentry/sentry-java/pull/5072))
+ - To enable creation of these spans, set `options.enableDatabaseTransactionTracing` to `true`
+ - `enable-database-transaction-tracing=true` when using `sentry.properties`
+ - For Spring Boot, use `sentry.enable-database-transaction-tracing=true` in `application.properties` or in `application.yml`:
+ ```yaml
+ sentry:
+ enable-database-transaction-tracing: true
+ ```
+- Add support for collecting native crashes using Tombstones ([#4933](https://github.com/getsentry/sentry-java/pull/4933), [#5037](https://github.com/getsentry/sentry-java/pull/5037))
+ - Added Tombstone integration that detects native crashes using `ApplicationExitInfo.REASON_CRASH_NATIVE` on Android 12+
+ - Crashes enriched with Tombstones contain more crash details and detailed thread info
+ - Tombstone and NDK integrations are now automatically merged into a single crash event, eliminating duplicate reports
+ - To enable it, add the integration in your Sentry initialization:
+ ```kotlin
+ SentryAndroid.init(context, options -> {
+ options.isTombstoneEnabled = true
+ })
+ ```
+ or in the `AndroidManifest.xml` using:
+ ```xml
+
+ ```
+
+### Fixes
+
+- Extract `SpotlightIntegration` to separate `sentry-spotlight` module to prevent insecure HTTP URLs from appearing in release APKs ([#5064](https://github.com/getsentry/sentry-java/pull/5064))
+ - **Breaking:** Users who enable Spotlight must now add the `io.sentry:sentry-spotlight` dependency:
+ ```kotlin
+ dependencies {
+ debugImplementation("io.sentry:sentry-spotlight:")
+ }
+ ```
+- Fix scroll target detection for Jetpack Compose ([#5017](https://github.com/getsentry/sentry-java/pull/5017))
+- No longer fork Sentry `Scopes` for `reactor-kafka` consumer poll `Runnable` ([#5080](https://github.com/getsentry/sentry-java/pull/5080))
+ - This was causing a memory leak because `reactor-kafka`'s poll event reschedules itself infinitely, and each invocation of `SentryScheduleHook` created forked scopes with a parent reference, building an unbounded chain that couldn't be garbage collected.
+- Fix cold/warm app start type detection for Android devices running API level 34+ ([#4999](https://github.com/getsentry/sentry-java/pull/4999))
+
+### Internal
+
+- Establish new native exception mechanisms to differentiate events generated by `sentry-native` from `ApplicationExitInfo`. ([#5052](https://github.com/getsentry/sentry-java/pull/5052))
+- Set `write` permission for `statuses` in the changelog preview GHA workflow. ([#5053](https://github.com/getsentry/sentry-java/pull/5053))
+
+### Dependencies
+
+- Bump Native SDK from v0.12.3 to v0.12.4 ([#5061](https://github.com/getsentry/sentry-java/pull/5061))
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0124)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.12.3...0.12.4)
+
+## 8.31.0
+
+### Features
+
+- Added `io.sentry.ndk.sdk-name` Android manifest option to configure the native SDK's name ([#5027](https://github.com/getsentry/sentry-java/pull/5027))
+- Replace `sentry.trace.parent_span_id` attribute with `spanId` property on `SentryLogEvent` ([#5040](https://github.com/getsentry/sentry-java/pull/5040))
+
+### Fixes
+
+- Only attach user attributes to logs if `sendDefaultPii` is enabled ([#5036](https://github.com/getsentry/sentry-java/pull/5036))
+- Reject new logs if `LoggerBatchProcessor` is shutting down ([#5041](https://github.com/getsentry/sentry-java/pull/5041))
+- Downgrade protobuf-javalite dependency from 4.33.1 to 3.25.8 ([#5044](https://github.com/getsentry/sentry-java/pull/5044))
+
+### Dependencies
+
+- Bump Native SDK from v0.12.2 to v0.12.3 ([#5012](https://github.com/getsentry/sentry-java/pull/5012))
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0123)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.12.2...0.12.3)
+
+## 8.30.0
+
+### Fixes
+
+- Fix ANRs when collecting device context ([#4970](https://github.com/getsentry/sentry-java/pull/4970))
+ - **IMPORTANT:** This disables collecting external storage size (total/free) by default, to enable it back
+ use `options.isCollectExternalStorageContext = true` or ` `
+- Fix `NullPointerException` when reading ANR marker ([#4979](https://github.com/getsentry/sentry-java/pull/4979))
+- Report discarded log in batch processor as `log_byte` ([#4971](https://github.com/getsentry/sentry-java/pull/4971))
+
+### Improvements
+
+- Expose `MAX_EVENT_SIZE_BYTES` constant in SentryOptions ([#4962](https://github.com/getsentry/sentry-java/pull/4962))
+- Discard envelopes on `4xx` and `5xx` response ([#4950](https://github.com/getsentry/sentry-java/pull/4950))
+ - This aims to not overwhelm Sentry after an outage or load shedding (including HTTP 429) where too many events are sent at once
+
+### Features
+
+- Add a Tombstone integration that detects native crashes without relying on the NDK integration, but instead using `ApplicationExitInfo.REASON_CRASH_NATIVE` on Android 12+. ([#4933](https://github.com/getsentry/sentry-java/pull/4933))
+ - Currently exposed via options as an _internal_ API only.
+ - If enabled alongside the NDK integration, crashes will be reported as two separate events. Users should enable only one; deduplication between both integrations will be added in a future release.
+- Add Sentry Metrics to Java SDK ([#5026](https://github.com/getsentry/sentry-java/pull/5026))
+ - Metrics are enabled by default
+ - APIs are namespaced under `Sentry.metrics()`
+ - We offer the following APIs:
+ - `count`: A metric that increments counts
+ - `gauge`: A metric that tracks a value that can go up or down
+ - `distribution`: A metric that tracks the statistical distribution of values
+ - For more details, see the Metrics documentation: https://docs.sentry.io/product/explore/metrics/getting-started/
+
+## 8.29.0
+
+### Fixes
+
+- Support serialization of primitive arrays (boolean[], byte[], short[], char[], int[], long[], float[], double[]) ([#4968](https://github.com/getsentry/sentry-java/pull/4968))
+- Session Replay: Improve network body parsing and truncation handling ([#4958](https://github.com/getsentry/sentry-java/pull/4958))
+
+### Internal
+
+- Support `metric` envelope item type ([#4956](https://github.com/getsentry/sentry-java/pull/4956))
+
+## 8.28.0
+
+### Features
+
+- Android: Flush logs when app enters background ([#4951](https://github.com/getsentry/sentry-java/pull/4951))
+- Add option to capture additional OkHttp network request/response details in session replays ([#4919](https://github.com/getsentry/sentry-java/pull/4919))
+ - Depends on `SentryOkHttpInterceptor` to intercept the request and extract request/response bodies
+ - To enable, add url regexes via the `io.sentry.session-replay.network-detail-allow-urls` metadata tag in AndroidManifest ([code sample](https://github.com/getsentry/sentry-java/blob/b03edbb1b0d8b871c62a09bc02cbd8a4e1f6fea1/sentry-samples/sentry-samples-android/src/main/AndroidManifest.xml#L196-L205)) - Or you can manually specify SentryReplayOptions via `SentryAndroid#init`:
+ _(Make sure you disable the auto init via manifest meta-data: io.sentry.auto-init=false)_
+
+
+ Kotlin
+
+```kotlin
+SentryAndroid.init(
+ this,
+ options -> {
+ // options.dsn = "https://examplePublicKey@o0.ingest.sentry.io/0"
+ // options.sessionReplay.sessionSampleRate = 1.0
+ // options.sessionReplay.onErrorSampleRate = 1.0
+ // ..
+
+ options.sessionReplay.networkDetailAllowUrls = listOf(".*")
+ options.sessionReplay.networkDetailDenyUrls = listOf(".*deny.*")
+ options.sessionReplay.networkRequestHeaders = listOf("Authorization", "X-Custom-Header", "X-Test-Request")
+ options.sessionReplay.networkResponseHeaders = listOf("X-Response-Time", "X-Cache-Status", "X-Test-Response")
+ });
+```
+
+
+
+
+ Java
+
+```java
+SentryAndroid.init(
+ this,
+ options -> {
+ options.getSessionReplay().setNetworkDetailAllowUrls(Arrays.asList(".*"));
+ options.getSessionReplay().setNetworkDetailDenyUrls(Arrays.asList(".*deny.*"));
+ options.getSessionReplay().setNetworkRequestHeaders(
+ Arrays.asList("Authorization", "X-Custom-Header", "X-Test-Request"));
+ options.getSessionReplay().setNetworkResponseHeaders(
+ Arrays.asList("X-Response-Time", "X-Cache-Status", "X-Test-Response"));
+ });
+
+```
+
+
+
+### Improvements
+
+- Avoid forking `rootScopes` for Reactor if current thread has `NoOpScopes` ([#4793](https://github.com/getsentry/sentry-java/pull/4793))
+ - This reduces the SDKs overhead by avoiding unnecessary scope forks
+
+### Fixes
+
+- Fix missing thread stacks for ANRv1 events ([#4918](https://github.com/getsentry/sentry-java/pull/4918))
+- Fix handling of unparseable mime-type on request filter ([#4939](https://github.com/getsentry/sentry-java/pull/4939))
+
+### Internal
+
+- Support `span` envelope item type ([#4935](https://github.com/getsentry/sentry-java/pull/4935))
+
+### Dependencies
+
+- Bump Native SDK from v0.12.1 to v0.12.2 ([#4944](https://github.com/getsentry/sentry-java/pull/4944))
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0122)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.12.1...0.12.2)
+
+## 8.27.1
+
+### Fixes
+
+- Do not log if `sentry.properties` in rundir has not been found ([#4929](https://github.com/getsentry/sentry-java/pull/4929))
+
+## 8.27.0
+
+### Features
+
+- Implement OpenFeature Integration that tracks Feature Flag evaluations ([#4910](https://github.com/getsentry/sentry-java/pull/4910))
+ - To make use of it, add the `sentry-openfeature` dependency and register the the hook using: `openFeatureApiInstance.addHooks(new SentryOpenFeatureHook());`
+- Implement LaunchDarkly Integrations that track Feature Flag evaluations ([#4917](https://github.com/getsentry/sentry-java/pull/4917))
+ - For Android, please add `sentry-launchdarkly-android` as a dependency and register the `SentryLaunchDarklyAndroidHook`
+ - For Server / JVM, please add `sentry-launchdarkly-server` as a dependency and register the `SentryLaunchDarklyServerHook`
+- Detect oversized events and reduce their size ([#4903](https://github.com/getsentry/sentry-java/pull/4903))
+ - You can opt into this new behaviour by setting `enableEventSizeLimiting` to `true` (`sentry.enable-event-size-limiting=true` for Spring Boot `application.properties`)
+ - You may optionally register an `onOversizedEvent` callback to implement custom logic that is executed in case an oversized event is detected
+ - This is executed first and if event size was reduced sufficiently, no further truncation is performed
+ - In case we detect an oversized event, we first drop breadcrumbs and if that isn't sufficient we also drop stack frames in order to get an events size down
+
+### Improvements
+
+- Do not send manual log origin ([#4897](https://github.com/getsentry/sentry-java/pull/4897))
+
+### Dependencies
+
+- Bump Spring Boot 4 to GA ([#4923](https://github.com/getsentry/sentry-java/pull/4923))
+
+## 8.26.0
+
+### Features
+
+- Add feature flags API ([#4812](https://github.com/getsentry/sentry-java/pull/4812)) and ([#4831](https://github.com/getsentry/sentry-java/pull/4831))
+ - You may now keep track of your feature flag evaluations and have them show up in Sentry.
+ - Top level API (`Sentry.addFeatureFlag("my-feature-flag", true);`) writes to scopes and the current span (if there is one)
+ - It is also possible to use API on `IScope`, `IScopes`, `ISpan` and `ITransaction` directly
+ - Feature flag evaluations tracked on scope(s) will be added to any errors reported to Sentry.
+ - The SDK keeps the latest 100 evaluations from scope(s), replacing old entries as new evaluations are added.
+ - For feature flag evaluations tracked on spans:
+ - Only 10 evaluations are tracked per span, existing flags are updated but new ones exceeding the limit are ignored
+ - Spans do not inherit evaluations from their parent
+- Drop log events once buffer hits hard limit ([#4889](https://github.com/getsentry/sentry-java/pull/4889))
+ - If we have 1000 log events queued up, we drop any new logs coming in to prevent OOM
+- Remove vendored code and upgrade to async profiler 4.2 ([#4856](https://github.com/getsentry/sentry-java/pull/4856))
+ - This adds support for JDK 23+
+
+### Fixes
+
+- Removed SentryExecutorService limit for delayed scheduled tasks ([#4846](https://github.com/getsentry/sentry-java/pull/4846))
+- Fix visual artifacts for the Canvas strategy on some devices ([#4861](https://github.com/getsentry/sentry-java/pull/4861))
+- [Config] Trim whitespace on properties path ([#4880](https://github.com/getsentry/sentry-java/pull/4880))
+- Only set `DefaultReplayBreadcrumbConverter` if replay is available ([#4888](https://github.com/getsentry/sentry-java/pull/4888))
+- Session Replay: Cache connection status instead of using blocking calls ([#4891](https://github.com/getsentry/sentry-java/pull/4891))
+- Fix log count in client reports ([#4869](https://github.com/getsentry/sentry-java/pull/4869))
+- Fix profilerId propagation ([#4833](https://github.com/getsentry/sentry-java/pull/4833))
+- Fix profiling init for Spring and Spring Boot w Agent auto-init ([#4815](https://github.com/getsentry/sentry-java/pull/4815))
+- Copy active span on scope clone ([#4878](https://github.com/getsentry/sentry-java/pull/4878))
+
+### Improvements
+
+- Fallback to distinct-id as user.id logging attribute when user is not set ([#4847](https://github.com/getsentry/sentry-java/pull/4847))
+- Report Timber.tag() as `timber.tag` log attribute ([#4845](https://github.com/getsentry/sentry-java/pull/4845))
+- Session Replay: Add screenshot strategy serialization to RRWeb events ([#4851](https://github.com/getsentry/sentry-java/pull/4851))
+- Report discarded log bytes ([#4871](https://github.com/getsentry/sentry-java/pull/4871))
+- Log why a properties file was not loaded ([#4879](https://github.com/getsentry/sentry-java/pull/4879))
+
+### Dependencies
+
+- Bump Native SDK from v0.11.3 to v0.12.1 ([#4859](https://github.com/getsentry/sentry-java/pull/4859))
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0121)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.11.3...0.12.1)
+- Bump Spring Boot 4 to RC2 ([#4886](https://github.com/getsentry/sentry-java/pull/4886))
+
+## 8.25.0
+
+### Fixes
+
+- [ANR] Removed AndroidTransactionProfiler lock ([#4817](https://github.com/getsentry/sentry-java/pull/4817))
+- Avoid ExecutorService for DefaultCompositePerformanceCollector timeout ([#4841](https://github.com/getsentry/sentry-java/pull/4841))
+ - This avoids infinite data collection for never stopped transactions, leading to OOMs
+- Fix wrong .super() call in SentryTimberTree ([#4844](https://github.com/getsentry/sentry-java/pull/4844))
+
+### Improvements
+
+- [ANR] Defer some class availability checks ([#4825](https://github.com/getsentry/sentry-java/pull/4825))
+- Collect PerformanceCollectionData only for sampled transactions ([#4834](https://github.com/getsentry/sentry-java/pull/4834))
+ - **Breaking change**: Transactions with a deferred sampling decision (`sampled == null`) won't be collecting any performance data anymore (CPU, RAM, slow/frozen frames).
+
+### Dependencies
+
+- Bump Native SDK from v0.11.2 to v0.11.3 ([#4810](https://github.com/getsentry/sentry-java/pull/4810))
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0113)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.11.2...0.11.3)
+
+## 8.24.0
+
+### Features
+
+- Attach MDC properties to logs as attributes ([#4786](https://github.com/getsentry/sentry-java/pull/4786))
+ - MDC properties set using supported logging frameworks (Logback, Log4j2, java.util.Logging) are now attached to structured logs as attributes.
+ - The attribute reflected on the log is `mdc.`, where `` is the original key in the MDC.
+ - This means that you will be able to filter/aggregate logs in the product based on these properties.
+ - Only properties with keys matching the configured `contextTags` are sent as log attributes.
+ - You can configure which properties are sent using `options.setContextTags` if initalizing manually, or by specifying a comma-separated list of keys with a `context-tags` entry in `sentry.properties` or `sentry.context-tags` in `application.properties`.
+ - Note that keys containing spaces are not supported.
+- Add experimental Sentry Android Distribution module for integrating with Sentry Build Distribution to check for and install updates ([#4804](https://github.com/getsentry/sentry-java/pull/4804))
+- Allow passing a different `Handler` to `SystemEventsBreadcrumbsIntegration` and `AndroidConnectionStatusProvider` so their callbacks are deliver to that handler ([#4808](https://github.com/getsentry/sentry-java/pull/4808))
+- Session Replay: Add new _experimental_ Canvas Capture Strategy ([#4777](https://github.com/getsentry/sentry-java/pull/4777))
+ - A new screenshot capture strategy that uses Android's Canvas API for more accurate and reliable text and image masking
+ - Any `.drawText()` or `.drawBitmap()` calls are replaced by rectangles, ensuring no text or images are present in the resulting output
+ - Note: If this strategy is used, all text and images will be masked, regardless of any masking configuration
+ - To enable this feature, set the `screenshotStrategy`, either via code:
+ ```kotlin
+ SentryAndroid.init(context) { options ->
+ options.sessionReplay.screenshotStrategy = ScreenshotStrategyType.CANVAS
+ }
+ ```
+ or AndroidManifest.xml:
+ ```xml
+
+
+
+ ```
+
+### Fixes
+
+- Avoid StrictMode warnings ([#4724](https://github.com/getsentry/sentry-java/pull/4724))
+- Use logger from options for JVM profiler ([#4771](https://github.com/getsentry/sentry-java/pull/4771))
+- Session Replay: Avoid deadlock when pausing replay if no connection ([#4788](https://github.com/getsentry/sentry-java/pull/4788))
+- Session Replay: Fix capturing roots with no windows ([#4805](https://github.com/getsentry/sentry-java/pull/4805))
+- Session Replay: Fix `java.lang.IllegalArgumentException: width and height must be > 0` ([#4805](https://github.com/getsentry/sentry-java/pull/4805))
+- Handle `NoOpScopes` in `Context` when starting a span through OpenTelemetry ([#4823](https://github.com/getsentry/sentry-java/pull/4823))
+ - This fixes "java.lang.IllegalArgumentException: The DSN is required" when combining WebFlux and OpenTelemetry
+- Session Replay: Do not use recycled screenshots for masking ([#4790](https://github.com/getsentry/sentry-java/pull/4790))
+ - This fixes native crashes seen in `Canvas.`/`ScreenshotRecorder.capture`
+- Session Replay: Ensure bitmaps are recycled properly ([#4820](https://github.com/getsentry/sentry-java/pull/4820))
+
+### Miscellaneous
+
+- Mark SentryClient(SentryOptions) constructor as not internal ([#4787](https://github.com/getsentry/sentry-java/pull/4787))
+
+### Dependencies
+
+- Bump Native SDK from v0.10.1 to v0.11.2 ([#4775](https://github.com/getsentry/sentry-java/pull/4775))
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0112)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.10.1...0.11.2)
+
+## 8.23.0
+
+### Features
+
+- Add session replay id to Sentry Logs ([#4740](https://github.com/getsentry/sentry-java/pull/4740))
+- Add support for continuous profiling of JVM applications on macOS and Linux ([#4556](https://github.com/getsentry/sentry-java/pull/4556))
+ - [Sentry continuous profiling](https://docs.sentry.io/product/explore/profiling/) on the JVM is using async-profiler under the hood.
+ - By default this feature is disabled. Set a profile sample rate and chose a lifecycle (see below) to enable it.
+ - Add the `sentry-async-profiler` dependency to your project
+ - Set a sample rate for profiles, e.g. `1.0` to send all of them. You may use `options.setProfileSessionSampleRate(1.0)` in code or `profile-session-sample-rate=1.0` in `sentry.properties`
+ - Set a profile lifecycle via `options.setProfileLifecycle(ProfileLifecycle.TRACE)` in code or `profile-lifecycle=TRACE` in `sentry.properties`
+ - By default the lifecycle is set to `MANUAL`, meaning you have to explicitly call `Sentry.startProfiler()` and `Sentry.stopProfiler()`
+ - You may change it to `TRACE` which will create a profile for each transaction
+ - To automatically upload Profiles for each transaction in a Spring Boot application
+ - set `sentry.profile-session-sample-rate=1.0` and `sentry.profile-lifecycle=TRACE` in `application.properties`
+ - or set `sentry.profile-session-sample-rate: 1.0` and `sentry.profile-lifecycle: TRACE` in `application.yml`
+ - Profiling can also be combined with our OpenTelemetry integration
+
+### Fixes
+
+- Start performance collection on AppStart continuous profiling ([#4752](https://github.com/getsentry/sentry-java/pull/4752))
+- Preserve modifiers in `SentryTraced` ([#4757](https://github.com/getsentry/sentry-java/pull/4757))
+
### Improvements
- Handle `RejectedExecutionException` everywhere ([#4747](https://github.com/getsentry/sentry-java/pull/4747))
@@ -72,15 +1149,15 @@
- Add onDiscard to enable users to track the type and amount of data discarded before reaching Sentry ([#4612](https://github.com/getsentry/sentry-java/pull/4612))
- Stub for setting the callback on `Sentry.init`:
- ```java
- Sentry.init(options -> {
- ...
- options.setOnDiscard(
- (reason, category, number) -> {
- // Your logic to process discarded data
- });
- });
- ```
+ ```java
+ Sentry.init(options -> {
+ ...
+ options.setOnDiscard(
+ (reason, category, number) -> {
+ // Your logic to process discarded data
+ });
+ });
+ ```
## 8.19.1
@@ -130,7 +1207,7 @@
- Move and flush unfinished previous session on init ([#4624](https://github.com/getsentry/sentry-java/pull/4624))
- This removes the need for unnecessary blocking our background queue for 15 seconds in the case of a background app start
- Switch to compileOnly dependency for compose-ui-material ([#4630](https://github.com/getsentry/sentry-java/pull/4630))
- - This fixes `StackOverflowError` when using OSS Licenses plugin
+ - This fixes `StackOverflowError` when using OSS Licenses plugin
### Dependencies
@@ -335,21 +1412,24 @@
### Features
- Add New User Feedback Widget ([#4450](https://github.com/getsentry/sentry-java/pull/4450))
- - This widget is a custom button that can be used to show the user feedback form
+ - This widget is a custom button that can be used to show the user feedback form
- Add New User Feedback form ([#4384](https://github.com/getsentry/sentry-java/pull/4384))
- - We now introduce SentryUserFeedbackDialog, which extends AlertDialog, inheriting the show() and cancel() methods, among others.
- To use it, just instantiate it and call show() on the instance (Sentry must be previously initialized).
- For customization options, please check the [User Feedback documentation](https://docs.sentry.io/platforms/android/user-feedback/configuration/).
- ```java
- import io.sentry.android.core.SentryUserFeedbackDialog;
-
- new SentryUserFeedbackDialog.Builder(context).create().show();
- ```
- ```kotlin
- import io.sentry.android.core.SentryUserFeedbackDialog
-
- SentryUserFeedbackDialog.Builder(context).create().show()
- ```
+ - We now introduce SentryUserFeedbackDialog, which extends AlertDialog, inheriting the show() and cancel() methods, among others.
+ To use it, just instantiate it and call show() on the instance (Sentry must be previously initialized).
+ For customization options, please check the [User Feedback documentation](https://docs.sentry.io/platforms/android/user-feedback/configuration/).
+
+ ```java
+ import io.sentry.android.core.SentryUserFeedbackDialog;
+
+ new SentryUserFeedbackDialog.Builder(context).create().show();
+ ```
+
+ ```kotlin
+ import io.sentry.android.core.SentryUserFeedbackDialog
+
+ SentryUserFeedbackDialog.Builder(context).create().show()
+ ```
+
- Add `user.id`, `user.name` and `user.email` to log attributes ([#4486](https://github.com/getsentry/sentry-java/pull/4486))
- User `name` attribute has been deprecated, please use `username` instead ([#4486](https://github.com/getsentry/sentry-java/pull/4486))
- Add device (`device.brand`, `device.model` and `device.family`) and OS (`os.name` and `os.version`) attributes to logs ([#4493](https://github.com/getsentry/sentry-java/pull/4493))
@@ -395,8 +1475,8 @@
### Features
- Add debug mode for Session Replay masking ([#4357](https://github.com/getsentry/sentry-java/pull/4357))
- - Use `Sentry.replay().enableDebugMaskingOverlay()` to overlay the screen with the Session Replay masks.
- - The masks will be invalidated at most once per `frameRate` (default 1 fps).
+ - Use `Sentry.replay().enableDebugMaskingOverlay()` to overlay the screen with the Session Replay masks.
+ - The masks will be invalidated at most once per `frameRate` (default 1 fps).
- Extend Logs API to allow passing in `attributes` ([#4402](https://github.com/getsentry/sentry-java/pull/4402))
- `Sentry.logger.log` now takes a `SentryLogParameters`
- Use `SentryLogParameters.create(SentryAttributes.of(...))` to pass attributes
@@ -427,17 +1507,17 @@
### Features
- Add new User Feedback API ([#4286](https://github.com/getsentry/sentry-java/pull/4286))
- - We now introduced Sentry.captureFeedback, which supersedes Sentry.captureUserFeedback
+ - We now introduced Sentry.captureFeedback, which supersedes Sentry.captureUserFeedback
- Add Sentry Log Feature ([#4372](https://github.com/getsentry/sentry-java/pull/4372))
- - The feature is disabled by default and needs to be enabled by:
- - `options.getLogs().setEnabled(true)` in `Sentry.init` / `SentryAndroid.init`
- - ` ` in `AndroidManifest.xml`
- - `logs.enabled=true` in `sentry.properties`
- - `sentry.logs.enabled=true` in `application.properties`
- - `sentry.logs.enabled: true` in `application.yml`
- - Logs can be captured using `Sentry.logger().info()` and similar methods.
- - Logs also take a format string and arguments which we then send through `String.format`.
- - Please use `options.getLogs().setBeforeSend()` to filter outgoing logs
+ - The feature is disabled by default and needs to be enabled by:
+ - `options.getLogs().setEnabled(true)` in `Sentry.init` / `SentryAndroid.init`
+ - ` ` in `AndroidManifest.xml`
+ - `logs.enabled=true` in `sentry.properties`
+ - `sentry.logs.enabled=true` in `application.properties`
+ - `sentry.logs.enabled: true` in `application.yml`
+ - Logs can be captured using `Sentry.logger().info()` and similar methods.
+ - Logs also take a format string and arguments which we then send through `String.format`.
+ - Please use `options.getLogs().setBeforeSend()` to filter outgoing logs
### Fixes
@@ -472,11 +1552,11 @@
### Features
- Wrap configured OpenTelemetry `ContextStorageProvider` if available ([#4359](https://github.com/getsentry/sentry-java/pull/4359))
- - This is only relevant if you see `java.lang.IllegalStateException: Found multiple ContextStorageProvider. Set the io.opentelemetry.context.ContextStorageProvider property to the fully qualified class name of the provider to use. Falling back to default ContextStorage. Found providers: ...`
+ - This is only relevant if you see `java.lang.IllegalStateException: Found multiple ContextStorageProvider. Set the io.opentelemetry.context.ContextStorageProvider property to the fully qualified class name of the provider to use. Falling back to default ContextStorage. Found providers: ...`
- Set `-Dio.opentelemetry.context.contextStorageProvider=io.sentry.opentelemetry.SentryContextStorageProvider` on your `java` command
- Sentry will then wrap the other `ContextStorageProvider` that has been configured by loading it through SPI
- If no other `ContextStorageProvider` is available or there are problems loading it, we fall back to using `SentryOtelThreadLocalStorage`
-
+
### Fixes
- Update profile chunk rate limit and client report ([#4353](https://github.com/getsentry/sentry-java/pull/4353))
@@ -535,9 +1615,9 @@
- UI Profiling GA
Continuous Profiling is now GA, named UI Profiling. To enable it you can use one of the following options. More info can be found at https://docs.sentry.io/platforms/android/profiling/.
- Note: Both `options.profilesSampler` and `options.profilesSampleRate` must **not** be set to enable UI Profiling.
- To keep the same transaction-based behaviour, without the 30 seconds limitation, you can use the `trace` lifecycle mode.
-
+ Note: Both `options.profilesSampler` and `options.profilesSampleRate` must **not** be set to enable UI Profiling.
+ To keep the same transaction-based behaviour, without the 30 seconds limitation, you can use the `trace` lifecycle mode.
+
```xml
@@ -548,10 +1628,11 @@
```
+
```java
import io.sentry.ProfileLifecycle;
import io.sentry.android.core.SentryAndroid;
-
+
SentryAndroid.init(context, options -> {
// Enable UI profiling, adjust in production env. This is evaluated only once per session
options.setProfileSessionSampleRate(1.0);
@@ -561,6 +1642,7 @@
options.setStartProfilerOnAppStart(true);
});
```
+
```kotlin
import io.sentry.ProfileLifecycle
import io.sentry.android.core.SentryAndroid
@@ -631,10 +1713,10 @@
### Features
- Add native stack frame address information and debug image metadata to ANR events ([#4061](https://github.com/getsentry/sentry-java/pull/4061))
- - This enables symbolication for stripped native code in ANRs
+ - This enables symbolication for stripped native code in ANRs
- Add Continuous Profiling Support ([#3710](https://github.com/getsentry/sentry-java/pull/3710))
- To enable Continuous Profiling use the `Sentry.startProfiler` and `Sentry.stopProfiler` experimental APIs. Sampling rate can be set through `options.profileSessionSampleRate`, which defaults to null (disabled).
+ To enable Continuous Profiling use the `Sentry.startProfiler` and `Sentry.stopProfiler` experimental APIs. Sampling rate can be set through `options.profileSessionSampleRate`, which defaults to null (disabled).
Note: Both `options.profilesSampler` and `options.profilesSampleRate` must **not** be set to enable Continuous Profiling.
```java
@@ -642,7 +1724,7 @@
import io.sentry.android.core.SentryAndroid;
SentryAndroid.init(context) { options ->
-
+
// Currently under experimental options:
options.getExperimental().setProfileSessionSampleRate(1.0);
// In manual mode, you need to start and stop the profiler manually using Sentry.startProfiler and Sentry.stopProfiler
@@ -651,16 +1733,17 @@
}
// Start profiling
Sentry.startProfiler();
-
+
// After all profiling is done, stop the profiler. Profiles can last indefinitely if not stopped.
Sentry.stopProfiler();
```
+
```kotlin
import io.sentry.ProfileLifecycle
import io.sentry.android.core.SentryAndroid
SentryAndroid.init(context) { options ->
-
+
// Currently under experimental options:
options.experimental.profileSessionSampleRate = 1.0
// In manual mode, you need to start and stop the profiler manually using Sentry.startProfiler and Sentry.stopProfiler
@@ -669,7 +1752,7 @@
}
// Start profiling
Sentry.startProfiler()
-
+
// After all profiling is done, stop the profiler. Profiles can last indefinitely if not stopped.
Sentry.stopProfiler()
```
@@ -707,7 +1790,7 @@
- remove any previous value if the new value is set to `null`
- Add support for setting in-app-includes/in-app-excludes via AndroidManifest.xml ([#4240](https://github.com/getsentry/sentry-java/pull/4240))
- Modifications to OkHttp requests are now properly propagated to the affected span / breadcrumbs ([#4238](https://github.com/getsentry/sentry-java/pull/4238))
- - Please ensure the SentryOkHttpInterceptor is added last to your OkHttpClient, as otherwise changes to the `Request` by subsequent interceptors won't be considered
+ - Please ensure the SentryOkHttpInterceptor is added last to your OkHttpClient, as otherwise changes to the `Request` by subsequent interceptors won't be considered
- Fix "class ch.qos.logback.classic.spi.ThrowableProxyVO cannot be cast to class ch.qos.logback.classic.spi.ThrowableProxy" ([#4206](https://github.com/getsentry/sentry-java/pull/4206))
- In this case we cannot report the `Throwable` to Sentry as it's not available
- If you are using OpenTelemetry v1 `OpenTelemetryAppender`, please consider upgrading to v2
@@ -780,7 +1863,7 @@
### Behavioural Changes
- The class `io.sentry.spring.jakarta.webflux.ReactorUtils` is now deprecated, please use `io.sentry.reactor.SentryReactorUtils` in the new `sentry-reactor` module instead ([#4155](https://github.com/getsentry/sentry-java/pull/4155))
- - The new module will be exposed as an `api` dependency when using `sentry-spring-boot-jakarta` (Spring Boot 3) or `sentry-spring-jakarta` (Spring 6).
+ - The new module will be exposed as an `api` dependency when using `sentry-spring-boot-jakarta` (Spring Boot 3) or `sentry-spring-jakarta` (Spring 6).
Therefore, if you're using one of those modules, changing your imports will suffice.
## 8.2.0
@@ -794,7 +1877,7 @@
- Create onCreate and onStart spans for all Activities ([#4025](https://github.com/getsentry/sentry-java/pull/4025))
- Add split apks info to the `App` context ([#3193](https://github.com/getsentry/sentry-java/pull/3193))
- Expose new `withSentryObservableEffect` method overload that accepts `SentryNavigationListener` as a parameter ([#4143](https://github.com/getsentry/sentry-java/pull/4143))
- - This allows sharing the same `SentryNavigationListener` instance across fragments and composables to preserve the trace
+ - This allows sharing the same `SentryNavigationListener` instance across fragments and composables to preserve the trace
- (Internal) Add API to filter native debug images based on stacktrace addresses ([#4089](https://github.com/getsentry/sentry-java/pull/4089))
- Propagate sampling random value ([#4153](https://github.com/getsentry/sentry-java/pull/4153))
- The random value used for sampling traces is now sent to Sentry and attached to the `baggage` header on outgoing requests
@@ -865,6 +1948,7 @@ SentryAndroid.init(context) { options ->
```
If you would like to keep some of the default broadcast events as breadcrumbs, consider opening a [GitHub issue](https://github.com/getsentry/sentry-java/issues/new).
+
- Set mechanism `type` to `suppressed` for suppressed exceptions ([#4125](https://github.com/getsentry/sentry-java/pull/4125))
- This helps to distinguish an exceptions cause from any suppressed exceptions in the Sentry UI
@@ -886,10 +1970,10 @@ Version 8 of the Sentry Android/Java SDK brings a variety of features and fixes.
- Lifecycle tokens have been introduced to manage `Scope` lifecycle, see "Behavioural Changes" for more details.
- Bumping `minSdk` level to 21 (Android 5.0)
- Our `sentry-opentelemetry-agent` has been improved and now works in combination with the rest of Sentry. You may now combine OpenTelemetry and Sentry for instrumenting your application.
- - You may now use both OpenTelemetry SDK and Sentry SDK to capture transactions and spans. They can also be mixed and end up on the same transaction.
- - OpenTelemetry extends the Sentry SDK by adding spans for numerous integrations, like Ktor, Vert.x and MongoDB. Please check [the OpenTelemetry GitHub repository](https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation) for a full list.
- - OpenTelemetry allows propagating trace information from and to additional libraries, that Sentry did not support before, for example gRPC.
- - OpenTelemetry also has broader support for propagating the Sentry `Scopes` through reactive libraries like RxJava.
+ - You may now use both OpenTelemetry SDK and Sentry SDK to capture transactions and spans. They can also be mixed and end up on the same transaction.
+ - OpenTelemetry extends the Sentry SDK by adding spans for numerous integrations, like Ktor, Vert.x and MongoDB. Please check [the OpenTelemetry GitHub repository](https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation) for a full list.
+ - OpenTelemetry allows propagating trace information from and to additional libraries, that Sentry did not support before, for example gRPC.
+ - OpenTelemetry also has broader support for propagating the Sentry `Scopes` through reactive libraries like RxJava.
- The SDK is now compatible with Spring Boot 3.4
- We now support GraphQL v22 (`sentry-graphql-22`)
- Metrics have been removed
@@ -906,11 +1990,11 @@ This SDK version is compatible with a self-hosted version of Sentry `22.12.0` or
- The minSdk level for sentry-android-ndk changed from 19 to 21 ([#3851](https://github.com/getsentry/sentry-java/pull/3851))
- Throw IllegalArgumentException when calling Sentry.init on Android ([#3596](https://github.com/getsentry/sentry-java/pull/3596))
- Metrics have been removed from the SDK ([#3774](https://github.com/getsentry/sentry-java/pull/3774))
- - Metrics will return but we don't know in what exact form yet
+ - Metrics will return but we don't know in what exact form yet
- `enableTracing` option (a.k.a `enable-tracing`) has been removed from the SDK ([#3776](https://github.com/getsentry/sentry-java/pull/3776))
- - Please set `tracesSampleRate` to a value >= 0.0 for enabling performance instead. The default value is `null` which means performance is disabled.
+ - Please set `tracesSampleRate` to a value >= 0.0 for enabling performance instead. The default value is `null` which means performance is disabled.
- Replace `synchronized` methods and blocks with `ReentrantLock` (`AutoClosableReentrantLock`) ([#3715](https://github.com/getsentry/sentry-java/pull/3715))
- - If you are subclassing any Sentry classes, please check if the parent class used `synchronized` before. Please make sure to use the same lock object as the parent class in that case.
+ - If you are subclassing any Sentry classes, please check if the parent class used `synchronized` before. Please make sure to use the same lock object as the parent class in that case.
- `traceOrigins` option (`io.sentry.traces.tracing-origins` in manifest) has been removed, please use `tracePropagationTargets` (`io.sentry.traces.trace-propagation-targets` in manifest`) instead ([#3780](https://github.com/getsentry/sentry-java/pull/3780))
- `profilingEnabled` option (`io.sentry.traces.profiling.enable` in manifest) has been removed, please use `profilesSampleRate` (`io.sentry.traces.profiling.sample-rate` instead) instead ([#3780](https://github.com/getsentry/sentry-java/pull/3780))
- `shutdownTimeout` option has been removed, please use `shutdownTimeoutMillis` instead ([#3780](https://github.com/getsentry/sentry-java/pull/3780))
@@ -930,32 +2014,32 @@ This SDK version is compatible with a self-hosted version of Sentry `22.12.0` or
- User segment has been removed ([#3512](https://github.com/getsentry/sentry-java/pull/3512))
- One of the `AndroidTransactionProfiler` constructors has been removed, please use a different one ([#3780](https://github.com/getsentry/sentry-java/pull/3780))
- Use String instead of UUID for SessionId ([#3834](https://github.com/getsentry/sentry-java/pull/3834))
- - The `Session` constructor now takes a `String` instead of a `UUID` for the `sessionId` parameter.
- - `Session.getSessionId()` now returns a `String` instead of a `UUID`.
+ - The `Session` constructor now takes a `String` instead of a `UUID` for the `sessionId` parameter.
+ - `Session.getSessionId()` now returns a `String` instead of a `UUID`.
- All status codes below 400 are now mapped to `SpanStatus.OK` ([#3869](https://github.com/getsentry/sentry-java/pull/3869))
- Change OkHttp sub-spans to span attributes ([#3556](https://github.com/getsentry/sentry-java/pull/3556))
- - This will reduce the number of spans created by the SDK
+ - This will reduce the number of spans created by the SDK
- `instrumenter` option should no longer be needed as our new OpenTelemetry integration now works in combination with the rest of Sentry
### Behavioural Changes
- We're introducing some new `Scope` types in the SDK, allowing for better control over what data is attached where. Previously there was a stack of scopes that was pushed and popped. Instead we now fork scopes for a given lifecycle and then restore the previous scopes. Since `Hub` is gone, it is also never cloned anymore. Separation of data now happens through the different scope types while making it easier to manipulate exactly what you need without having to attach data at the right time to have it apply where wanted.
- - Global scope is attached to all events created by the SDK. It can also be modified before `Sentry.init` has been called. It can be manipulated using `Sentry.configureScope(ScopeType.GLOBAL, (scope) -> { ... })`.
- - Isolation scope can be used e.g. to attach data to all events that come up while handling an incoming request. It can also be used for other isolation purposes. It can be manipulated using `Sentry.configureScope(ScopeType.ISOLATION, (scope) -> { ... })`. The SDK automatically forks isolation scope in certain cases like incoming requests, CRON jobs, Spring `@Async` and more.
- - Current scope is forked often and data added to it is only added to events that are created while this scope is active. Data is also passed on to newly forked child scopes but not to parents. It can be manipulated using `Sentry.configureScope(ScopeType.CURRENT, (scope) -> { ... })`.
+ - Global scope is attached to all events created by the SDK. It can also be modified before `Sentry.init` has been called. It can be manipulated using `Sentry.configureScope(ScopeType.GLOBAL, (scope) -> { ... })`.
+ - Isolation scope can be used e.g. to attach data to all events that come up while handling an incoming request. It can also be used for other isolation purposes. It can be manipulated using `Sentry.configureScope(ScopeType.ISOLATION, (scope) -> { ... })`. The SDK automatically forks isolation scope in certain cases like incoming requests, CRON jobs, Spring `@Async` and more.
+ - Current scope is forked often and data added to it is only added to events that are created while this scope is active. Data is also passed on to newly forked child scopes but not to parents. It can be manipulated using `Sentry.configureScope(ScopeType.CURRENT, (scope) -> { ... })`.
- `Sentry.popScope` has been deprecated, please call `.close()` on the token returned by `Sentry.pushScope` instead or use it in a way described in more detail in [our migration guide](https://docs.sentry.io/platforms/java/migration/7.x-to-8.0).
- We have chosen a default scope that is used for `Sentry.configureScope()` as well as API like `Sentry.setTag()`
- - For Android the type defaults to `CURRENT` scope
- - For Backend and other JVM applicatons it defaults to `ISOLATION` scope
+ - For Android the type defaults to `CURRENT` scope
+ - For Backend and other JVM applicatons it defaults to `ISOLATION` scope
- Event processors on `Scope` can now be ordered by overriding the `getOrder` method on implementations of `EventProcessor`. NOTE: This order only applies to event processors on `Scope` but not `SentryOptions` at the moment. Feel free to request this if you need it.
- `Hub` is deprecated in favor of `Scopes`, alongside some `Hub` relevant APIs. More details can be found in [our migration guide](https://docs.sentry.io/platforms/java/migration/7.x-to-8.0).
- Send file name and path only if `isSendDefaultPii` is `true` ([#3919](https://github.com/getsentry/sentry-java/pull/3919))
- (Android) Enable Performance V2 by default ([#3824](https://github.com/getsentry/sentry-java/pull/3824))
- - With this change cold app start spans will include spans for ContentProviders, Application and Activity load.
+ - With this change cold app start spans will include spans for ContentProviders, Application and Activity load.
- (Android) Replace thread id with kernel thread id in span data ([#3706](https://github.com/getsentry/sentry-java/pull/3706))
- (Android) The JNI layer for sentry-native has now been moved from sentry-java to sentry-native ([#3189](https://github.com/getsentry/sentry-java/pull/3189))
- - This now includes prefab support for sentry-native, allowing you to link and access the sentry-native API within your native app code
- - Checkout the `sentry-samples/sentry-samples-android` example on how to configure CMake and consume `sentry.h`
+ - This now includes prefab support for sentry-native, allowing you to link and access the sentry-native API within your native app code
+ - Checkout the `sentry-samples/sentry-samples-android` example on how to configure CMake and consume `sentry.h`
- The user ip-address is now only set to `"{{auto}}"` if `sendDefaultPii` is enabled ([#4072](https://github.com/getsentry/sentry-java/pull/4072))
- This change gives you control over IP address collection directly on the client
@@ -963,49 +2047,49 @@ This SDK version is compatible with a self-hosted version of Sentry `22.12.0` or
- The SDK is now compatible with Spring Boot 3.4 ([#3939](https://github.com/getsentry/sentry-java/pull/3939))
- Our `sentry-opentelemetry-agent` has been completely reworked and now plays nicely with the rest of the Java SDK
- - You may also want to give this new agent a try even if you haven't used OpenTelemetry (with Sentry) before. It offers support for [many more libraries and frameworks](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/docs/supported-libraries.md), improving on our trace propagation, `Scopes` (used to be `Hub`) propagation as well as performance instrumentation (i.e. more spans).
- - If you are using a framework we did not support before and currently resort to manual instrumentation, please give the agent a try. See [here for a list of supported libraries, frameworks and application servers](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/docs/supported-libraries.md).
- - Please see [Java SDK docs](https://docs.sentry.io/platforms/java/tracing/instrumentation/opentelemetry/) for more details on how to set up the agent. Please make sure to select the correct SDK from the dropdown on the left side of the docs.
- - What's new about the Agent
- - When the OpenTelemetry Agent is used, Sentry API creates OpenTelemetry spans under the hood, handing back a wrapper object which bridges the gap between traditional Sentry API and OpenTelemetry. We might be replacing some of the Sentry performance API in the future.
- - This is achieved by configuring the SDK to use `OtelSpanFactory` instead of `DefaultSpanFactory` which is done automatically by the auto init of the Java Agent.
- - OpenTelemetry spans are now only turned into Sentry spans when they are finished so they can be sent to the Sentry server.
- - Now registers an OpenTelemetry `Sampler` which uses Sentry sampling configuration
- - Other Performance integrations automatically stop creating spans to avoid duplicate spans
- - The Sentry SDK now makes use of OpenTelemetry `Context` for storing Sentry `Scopes` (which is similar to what used to be called `Hub`) and thus relies on OpenTelemetry for `Context` propagation.
- - Classes used for the previous version of our OpenTelemetry support have been deprecated but can still be used manually. We're not planning to keep the old agent around in favor of less complexity in the SDK.
+ - You may also want to give this new agent a try even if you haven't used OpenTelemetry (with Sentry) before. It offers support for [many more libraries and frameworks](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/docs/supported-libraries.md), improving on our trace propagation, `Scopes` (used to be `Hub`) propagation as well as performance instrumentation (i.e. more spans).
+ - If you are using a framework we did not support before and currently resort to manual instrumentation, please give the agent a try. See [here for a list of supported libraries, frameworks and application servers](https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/docs/supported-libraries.md).
+ - Please see [Java SDK docs](https://docs.sentry.io/platforms/java/tracing/instrumentation/opentelemetry/) for more details on how to set up the agent. Please make sure to select the correct SDK from the dropdown on the left side of the docs.
+ - What's new about the Agent
+ - When the OpenTelemetry Agent is used, Sentry API creates OpenTelemetry spans under the hood, handing back a wrapper object which bridges the gap between traditional Sentry API and OpenTelemetry. We might be replacing some of the Sentry performance API in the future.
+ - This is achieved by configuring the SDK to use `OtelSpanFactory` instead of `DefaultSpanFactory` which is done automatically by the auto init of the Java Agent.
+ - OpenTelemetry spans are now only turned into Sentry spans when they are finished so they can be sent to the Sentry server.
+ - Now registers an OpenTelemetry `Sampler` which uses Sentry sampling configuration
+ - Other Performance integrations automatically stop creating spans to avoid duplicate spans
+ - The Sentry SDK now makes use of OpenTelemetry `Context` for storing Sentry `Scopes` (which is similar to what used to be called `Hub`) and thus relies on OpenTelemetry for `Context` propagation.
+ - Classes used for the previous version of our OpenTelemetry support have been deprecated but can still be used manually. We're not planning to keep the old agent around in favor of less complexity in the SDK.
- Add `sentry-opentelemetry-agentless-spring` module ([#4000](https://github.com/getsentry/sentry-java/pull/4000))
- - This module can be added as a dependency when using Sentry with OpenTelemetry and Spring Boot but don't want to use our Agent. It takes care of configuring OpenTelemetry for use with Sentry.
- - You may want to set `OTEL_LOGS_EXPORTER=none;OTEL_METRICS_EXPORTER=none;OTEL_TRACES_EXPORTER=none` env vars to not have the log flooded with error messages regarding OpenTelemetry features we don't use.
+ - This module can be added as a dependency when using Sentry with OpenTelemetry and Spring Boot but don't want to use our Agent. It takes care of configuring OpenTelemetry for use with Sentry.
+ - You may want to set `OTEL_LOGS_EXPORTER=none;OTEL_METRICS_EXPORTER=none;OTEL_TRACES_EXPORTER=none` env vars to not have the log flooded with error messages regarding OpenTelemetry features we don't use.
- Add `sentry-opentelemetry-agentless` module ([#3961](https://github.com/getsentry/sentry-java/pull/3961))
- - This module can be added as a dependency when using Sentry with OpenTelemetry but don't want to use our Agent. It takes care of configuring OpenTelemetry for use with Sentry.
- - To enable the auto configuration of it, please set `-Dotel.java.global-autoconfigure.enabled=true` on the `java` command, when starting your application.
- - You may also want to set `OTEL_LOGS_EXPORTER=none;OTEL_METRICS_EXPORTER=none;OTEL_TRACES_EXPORTER=none` env vars to not have the log flooded with error messages regarding OpenTelemetry features we don't use.
+ - This module can be added as a dependency when using Sentry with OpenTelemetry but don't want to use our Agent. It takes care of configuring OpenTelemetry for use with Sentry.
+ - To enable the auto configuration of it, please set `-Dotel.java.global-autoconfigure.enabled=true` on the `java` command, when starting your application.
+ - You may also want to set `OTEL_LOGS_EXPORTER=none;OTEL_METRICS_EXPORTER=none;OTEL_TRACES_EXPORTER=none` env vars to not have the log flooded with error messages regarding OpenTelemetry features we don't use.
- `OpenTelemetryUtil.applyOpenTelemetryOptions` now takes an enum instead of a boolean for its mode
- Add `openTelemetryMode` option ([#3994](https://github.com/getsentry/sentry-java/pull/3994))
- - It defaults to `AUTO` meaning the SDK will figure out how to best configure itself for use with OpenTelemetry
- - Use of OpenTelemetry can also be disabled completely by setting it to `OFF` ([#3995](https://github.com/getsentry/sentry-java/pull/3995))
- - In this case even if OpenTelemetry is present, the Sentry SDK will not use it
- - Use `AGENT` when using `sentry-opentelemetry-agent`
- - Use `AGENTLESS` when using `sentry-opentelemetry-agentless`
- - Use `AGENTLESS_SPRING` when using `sentry-opentelemetry-agentless-spring`
+ - It defaults to `AUTO` meaning the SDK will figure out how to best configure itself for use with OpenTelemetry
+ - Use of OpenTelemetry can also be disabled completely by setting it to `OFF` ([#3995](https://github.com/getsentry/sentry-java/pull/3995))
+ - In this case even if OpenTelemetry is present, the Sentry SDK will not use it
+ - Use `AGENT` when using `sentry-opentelemetry-agent`
+ - Use `AGENTLESS` when using `sentry-opentelemetry-agentless`
+ - Use `AGENTLESS_SPRING` when using `sentry-opentelemetry-agentless-spring`
- Add `ignoredTransactions` option to filter out transactions by name ([#3871](https://github.com/getsentry/sentry-java/pull/3871))
- - can be used via ENV vars, e.g. `SENTRY_IGNORED_TRANSACTIONS=POST /person/,GET /pers.*`
- - can also be set in options directly, e.g. `options.setIgnoredTransactions(...)`
- - can also be set in `sentry.properties`, e.g. `ignored-transactions=POST /person/,GET /pers.*`
- - can also be set in Spring config `application.properties`, e.g. `sentry.ignored-transactions=POST /person/,GET /pers.*`
+ - can be used via ENV vars, e.g. `SENTRY_IGNORED_TRANSACTIONS=POST /person/,GET /pers.*`
+ - can also be set in options directly, e.g. `options.setIgnoredTransactions(...)`
+ - can also be set in `sentry.properties`, e.g. `ignored-transactions=POST /person/,GET /pers.*`
+ - can also be set in Spring config `application.properties`, e.g. `sentry.ignored-transactions=POST /person/,GET /pers.*`
- Add `scopeBindingMode` to `SpanOptions` ([#4004](https://github.com/getsentry/sentry-java/pull/4004))
- - This setting only affects the SDK when used with OpenTelemetry.
- - Defaults to `AUTO` meaning the SDK will decide whether the span should be bound to the current scope. It will not bind transactions to scope using `AUTO`, it will only bind spans where the parent span is on the current scope.
- - `ON` sets the new span on the current scope.
- - `OFF` does not set the new span on the scope.
+ - This setting only affects the SDK when used with OpenTelemetry.
+ - Defaults to `AUTO` meaning the SDK will decide whether the span should be bound to the current scope. It will not bind transactions to scope using `AUTO`, it will only bind spans where the parent span is on the current scope.
+ - `ON` sets the new span on the current scope.
+ - `OFF` does not set the new span on the scope.
- Add `ignoredSpanOrigins` option for ignoring spans coming from certain integrations
- - We pre-configure this to ignore Performance instrumentation for Spring and other integrations when using our OpenTelemetry Agent to avoid duplicate spans
+ - We pre-configure this to ignore Performance instrumentation for Spring and other integrations when using our OpenTelemetry Agent to avoid duplicate spans
- Support `graphql-java` v22 via a new module `sentry-graphql-22` ([#3740](https://github.com/getsentry/sentry-java/pull/3740))
- - If you are using `graphql-java` v21 or earlier, you can use the `sentry-graphql` module
- - For `graphql-java` v22 and newer please use the `sentry-graphql-22` module
+ - If you are using `graphql-java` v21 or earlier, you can use the `sentry-graphql` module
+ - For `graphql-java` v22 and newer please use the `sentry-graphql-22` module
- We now provide a `SentryInstrumenter` bean directly for Spring (Boot) if there is none yet instead of using `GraphQlSourceBuilderCustomizer` to add the instrumentation ([#3744](https://github.com/getsentry/sentry-java/pull/3744))
- - It is now also possible to provide a bean of type `SentryGraphqlInstrumentation.BeforeSpanCallback` which is then used by `SentryInstrumenter`
+ - It is now also possible to provide a bean of type `SentryGraphqlInstrumentation.BeforeSpanCallback` which is then used by `SentryInstrumenter`
- Add data fetching environment hint to breadcrumb for GraphQL (#3413) ([#3431](https://github.com/getsentry/sentry-java/pull/3431))
- Report exceptions returned by Throwable.getSuppressed() to Sentry as exception groups ([#3396] https://github.com/getsentry/sentry-java/pull/3396)
- Any suppressed exceptions are added to the issue details page in Sentry, the same way any cause is.
@@ -1013,23 +2097,23 @@ This SDK version is compatible with a self-hosted version of Sentry `22.12.0` or
- Enable `ThreadLocalAccessor` for Spring Boot 3 WebFlux by default ([#4023](https://github.com/getsentry/sentry-java/pull/4023))
- Allow passing `environment` to `CheckinUtils.withCheckIn` ([3889](https://github.com/getsentry/sentry-java/pull/3889))
- Add `globalHubMode` to options ([#3805](https://github.com/getsentry/sentry-java/pull/3805))
- - `globalHubMode` used to only be a param on `Sentry.init`. To make it easier to be used in e.g. Desktop environments, we now additionally added it as an option on SentryOptions that can also be set via `sentry.properties`.
- - If both the param on `Sentry.init` and the option are set, the option will win. By default the option is set to `null` meaning whatever is passed to `Sentry.init` takes effect.
+ - `globalHubMode` used to only be a param on `Sentry.init`. To make it easier to be used in e.g. Desktop environments, we now additionally added it as an option on SentryOptions that can also be set via `sentry.properties`.
+ - If both the param on `Sentry.init` and the option are set, the option will win. By default the option is set to `null` meaning whatever is passed to `Sentry.init` takes effect.
- Lazy uuid generation for SentryId and SpanId ([#3770](https://github.com/getsentry/sentry-java/pull/3770))
- Faster generation of Sentry and Span IDs ([#3818](https://github.com/getsentry/sentry-java/pull/3818))
- - Uses faster implementation to convert UUID to SentryID String
- - Uses faster Random implementation to generate UUIDs
+ - Uses faster implementation to convert UUID to SentryID String
+ - Uses faster Random implementation to generate UUIDs
- Android 15: Add support for 16KB page sizes ([#3851](https://github.com/getsentry/sentry-java/pull/3851))
- - See https://developer.android.com/guide/practices/page-sizes for more details
+ - See https://developer.android.com/guide/practices/page-sizes for more details
- Add init priority settings ([#3674](https://github.com/getsentry/sentry-java/pull/3674))
- - You may now set `forceInit=true` (`force-init` for `.properties` files) to ensure a call to Sentry.init / SentryAndroid.init takes effect
+ - You may now set `forceInit=true` (`force-init` for `.properties` files) to ensure a call to Sentry.init / SentryAndroid.init takes effect
- Add force init option to Android Manifest ([#3675](https://github.com/getsentry/sentry-java/pull/3675))
- - Use ` ` to ensure Sentry Android auto init is not easily overwritten
+ - Use ` ` to ensure Sentry Android auto init is not easily overwritten
- Attach request body for `application/x-www-form-urlencoded` requests in Spring ([#3731](https://github.com/getsentry/sentry-java/pull/3731))
- - Previously request body was only attached for `application/json` requests
+ - Previously request body was only attached for `application/json` requests
- Set breadcrumb level based on http status ([#3771](https://github.com/getsentry/sentry-java/pull/3771))
- Emit transaction.data inside contexts.trace.data ([#3735](https://github.com/getsentry/sentry-java/pull/3735))
- - Also does not emit `transaction.data` in `extras` anymore
+ - Also does not emit `transaction.data` in `extras` anymore
- Add a sample for showcasing Sentry with OpenTelemetry for Spring Boot 3 with our Java agent (`sentry-samples-spring-boot-jakarta-opentelemetry`) ([#3856](https://github.com/getsentry/sentry-java/pull/3828))
- Add a sample for showcasing Sentry with OpenTelemetry for Spring Boot 3 without our Java agent (`sentry-samples-spring-boot-jakarta-opentelemetry-noagent`) ([#3856](https://github.com/getsentry/sentry-java/pull/3856))
- Add a sample for showcasing Sentry with OpenTelemetry (`sentry-samples-console-opentelemetry-noagent`) ([#3856](https://github.com/getsentry/sentry-java/pull/3862))
@@ -1037,28 +2121,28 @@ This SDK version is compatible with a self-hosted version of Sentry `22.12.0` or
### Fixes
- Fix incoming defer sampling decision `sentry-trace` header ([#3942](https://github.com/getsentry/sentry-java/pull/3942))
- - A `sentry-trace` header that only contains trace ID and span ID but no sampled flag (`-1`, `-0` suffix) means the receiving system can make its own sampling decision
- - When generating `sentry-trace` header from `PropagationContext` we now copy the `sampled` flag.
- - In `TransactionContext.fromPropagationContext` when there is no parent sampling decision, keep the decision `null` so a new sampling decision is made instead of defaulting to `false`
+ - A `sentry-trace` header that only contains trace ID and span ID but no sampled flag (`-1`, `-0` suffix) means the receiving system can make its own sampling decision
+ - When generating `sentry-trace` header from `PropagationContext` we now copy the `sampled` flag.
+ - In `TransactionContext.fromPropagationContext` when there is no parent sampling decision, keep the decision `null` so a new sampling decision is made instead of defaulting to `false`
- Fix order of calling `close` on previous Sentry instance when re-initializing ([#3750](https://github.com/getsentry/sentry-java/pull/3750))
- - Previously some parts of Sentry were immediately closed after re-init that should have stayed open and some parts of the previous init were never closed
+ - Previously some parts of Sentry were immediately closed after re-init that should have stayed open and some parts of the previous init were never closed
- All status codes below 400 are now mapped to `SpanStatus.OK` ([#3869](https://github.com/getsentry/sentry-java/pull/3869))
- Improve ignored check performance ([#3992](https://github.com/getsentry/sentry-java/pull/3992))
- - Checking if a span origin, a transaction or a checkIn should be ignored is now faster
+ - Checking if a span origin, a transaction or a checkIn should be ignored is now faster
- Cache requests for Spring using Springs `ContentCachingRequestWrapper` instead of our own Wrapper to also cache parameters ([#3641](https://github.com/getsentry/sentry-java/pull/3641))
- - Previously only the body was cached which could lead to problems in the FilterChain as Request parameters were not available
+ - Previously only the body was cached which could lead to problems in the FilterChain as Request parameters were not available
- Close backpressure monitor on SDK shutdown ([#3998](https://github.com/getsentry/sentry-java/pull/3998))
- - Due to the backpressure monitor rescheduling a task to run every 10s, it very likely caused shutdown to wait the full `shutdownTimeoutMillis` (defaulting to 2s) instead of being able to terminate immediately
+ - Due to the backpressure monitor rescheduling a task to run every 10s, it very likely caused shutdown to wait the full `shutdownTimeoutMillis` (defaulting to 2s) instead of being able to terminate immediately
- Let OpenTelemetry auto instrumentation handle extracting and injecting tracing information if present ([#3953](https://github.com/getsentry/sentry-java/pull/3953))
- - Our integrations no longer call `.continueTrace` and also do not inject tracing headers if the integration has been added to `ignoredSpanOrigins`
+ - Our integrations no longer call `.continueTrace` and also do not inject tracing headers if the integration has been added to `ignoredSpanOrigins`
- Fix testTag not working for Jetpack Compose user interaction tracking ([#3878](https://github.com/getsentry/sentry-java/pull/3878))
- Mark `DiskFlushNotification` hint flushed when rate limited ([#3892](https://github.com/getsentry/sentry-java/pull/3892))
- - Our `UncaughtExceptionHandlerIntegration` waited for the full flush timeout duration (default 15s) when rate limited.
+ - Our `UncaughtExceptionHandlerIntegration` waited for the full flush timeout duration (default 15s) when rate limited.
- Do not replace `op` with auto generated content for OpenTelemetry spans with span kind `INTERNAL` ([#3906](https://github.com/getsentry/sentry-java/pull/3906))
- Add `enable-spotlight` and `spotlight-connection-url` to external options and check if spotlight is enabled when deciding whether to inspect an OpenTelemetry span for connecting to splotlight ([#3709](https://github.com/getsentry/sentry-java/pull/3709))
- Trace context on `Contexts.setTrace` has been marked `@NotNull` ([#3721](https://github.com/getsentry/sentry-java/pull/3721))
- - Setting it to `null` would cause an exception.
- - Transactions are dropped if trace context is missing
+ - Setting it to `null` would cause an exception.
+ - Transactions are dropped if trace context is missing
- Remove internal annotation on `SpanOptions` ([#3722](https://github.com/getsentry/sentry-java/pull/3722))
- `SentryLogbackInitializer` is now public ([#3723](https://github.com/getsentry/sentry-java/pull/3723))
- Parse and use `send-default-pii` and `max-request-body-size` from `sentry.properties` ([#3534](https://github.com/getsentry/sentry-java/pull/3534))
@@ -1075,66 +2159,66 @@ These changes have been made during development of `8.0.0`. You may skip this se
- Extract OpenTelemetry `URL_PATH` span attribute into description ([#3933](https://github.com/getsentry/sentry-java/pull/3933))
- Replace OpenTelemetry `ContextStorage` wrapper with `ContextStorageProvider` ([#3938](https://github.com/getsentry/sentry-java/pull/3938))
- - The wrapper had to be put in place before any call to `Context` whereas `ContextStorageProvider` is automatically invoked at the correct time.
+ - The wrapper had to be put in place before any call to `Context` whereas `ContextStorageProvider` is automatically invoked at the correct time.
- Send `otel.kind` to Sentry ([#3907](https://github.com/getsentry/sentry-java/pull/3907))
- Spring Boot now automatically detects if OpenTelemetry is available and makes use of it ([#3846](https://github.com/getsentry/sentry-java/pull/3846))
- - This is only enabled if there is no OpenTelemetry agent available
- - We prefer to use the OpenTelemetry agent as it offers more auto instrumentation
- - In some cases the OpenTelemetry agent cannot be used, please see https://opentelemetry.io/docs/zero-code/java/spring-boot-starter/ for more details on when to prefer the Agent and when the Spring Boot starter makes more sense.
- - In this mode the SDK makes use of the `OpenTelemetry` bean that is created by `opentelemetry-spring-boot-starter` instead of `GlobalOpenTelemetry`
+ - This is only enabled if there is no OpenTelemetry agent available
+ - We prefer to use the OpenTelemetry agent as it offers more auto instrumentation
+ - In some cases the OpenTelemetry agent cannot be used, please see https://opentelemetry.io/docs/zero-code/java/spring-boot-starter/ for more details on when to prefer the Agent and when the Spring Boot starter makes more sense.
+ - In this mode the SDK makes use of the `OpenTelemetry` bean that is created by `opentelemetry-spring-boot-starter` instead of `GlobalOpenTelemetry`
- Spring Boot now automatically detects our OpenTelemetry agent if its auto init is disabled ([#3848](https://github.com/getsentry/sentry-java/pull/3848))
- - This means Spring Boot config mechanisms can now be combined with our OpenTelemetry agent
- - The `sentry-opentelemetry-extra` module has been removed again, most classes have been moved to `sentry-opentelemetry-bootstrap` which is loaded into the bootstrap classloader (i.e. `null`) when our Java agent is used. The rest has been moved into `sentry-opentelemetry-agentcustomization` and is loaded into the agent classloader when our Java agent is used.
- - The `sentry-opentelemetry-bootstrap` and `sentry-opentelemetry-agentcustomization` modules can be used without the agent as well, in which case all classes are loaded into the application classloader. Check out our `sentry-samples-spring-boot-jakarta-opentelemetry-noagent` sample.
- - In this mode the SDK makes use of `GlobalOpenTelemetry`
+ - This means Spring Boot config mechanisms can now be combined with our OpenTelemetry agent
+ - The `sentry-opentelemetry-extra` module has been removed again, most classes have been moved to `sentry-opentelemetry-bootstrap` which is loaded into the bootstrap classloader (i.e. `null`) when our Java agent is used. The rest has been moved into `sentry-opentelemetry-agentcustomization` and is loaded into the agent classloader when our Java agent is used.
+ - The `sentry-opentelemetry-bootstrap` and `sentry-opentelemetry-agentcustomization` modules can be used without the agent as well, in which case all classes are loaded into the application classloader. Check out our `sentry-samples-spring-boot-jakarta-opentelemetry-noagent` sample.
+ - In this mode the SDK makes use of `GlobalOpenTelemetry`
- Automatically set span factory based on presence of OpenTelemetry ([#3858](https://github.com/getsentry/sentry-java/pull/3858))
- - `SentrySpanFactoryHolder` has been removed as it is no longer required.
+ - `SentrySpanFactoryHolder` has been removed as it is no longer required.
- Replace deprecated `SimpleInstrumentation` with `SimplePerformantInstrumentation` for graphql 22 ([#3974](https://github.com/getsentry/sentry-java/pull/3974))
- We now hold a strong reference to the underlying OpenTelemetry span when it is created through Sentry API ([#3997](https://github.com/getsentry/sentry-java/pull/3997))
- - This keeps it from being garbage collected too early
+ - This keeps it from being garbage collected too early
- Defer sampling decision by setting `sampled` to `null` in `PropagationContext` when using OpenTelemetry in case of an incoming defer sampling `sentry-trace` header. ([#3945](https://github.com/getsentry/sentry-java/pull/3945))
- Build `PropagationContext` from `SamplingDecision` made by `SentrySampler` instead of parsing headers and potentially ignoring a sampling decision in case a `sentry-trace` header comes in with deferred sampling decision. ([#3947](https://github.com/getsentry/sentry-java/pull/3947))
- The Sentry OpenTelemetry Java agent now makes sure Sentry `Scopes` storage is initialized even if the agents auto init is disabled ([#3848](https://github.com/getsentry/sentry-java/pull/3848))
- - This is required for all integrations to work together with our OpenTelemetry Java agent if its auto init has been disabled and the SDKs init should be used instead.
+ - This is required for all integrations to work together with our OpenTelemetry Java agent if its auto init has been disabled and the SDKs init should be used instead.
- Fix `startChild` for span that is not in current OpenTelemetry `Context` ([#3862](https://github.com/getsentry/sentry-java/pull/3862))
- - Starting a child span from a transaction that wasn't in the current `Context` lead to multiple transactions being created (one for the transaction and another per span created).
+ - Starting a child span from a transaction that wasn't in the current `Context` lead to multiple transactions being created (one for the transaction and another per span created).
- Add `auto.graphql.graphql22` to ignored span origins when using OpenTelemetry ([#3828](https://github.com/getsentry/sentry-java/pull/3828))
- Use OpenTelemetry span name as fallback for transaction name ([#3557](https://github.com/getsentry/sentry-java/pull/3557))
- - In certain cases we were sending transactions as "" when using OpenTelemetry
+ - In certain cases we were sending transactions as "" when using OpenTelemetry
- Add OpenTelemetry span data to Sentry span ([#3593](https://github.com/getsentry/sentry-java/pull/3593))
- No longer selectively copy OpenTelemetry attributes to Sentry spans / transactions `data` ([#3663](https://github.com/getsentry/sentry-java/pull/3663))
- Remove `PROCESS_COMMAND_ARGS` (`process.command_args`) OpenTelemetry span attribute as it can be very large ([#3664](https://github.com/getsentry/sentry-java/pull/3664))
- Use RECORD_ONLY sampling decision if performance is disabled ([#3659](https://github.com/getsentry/sentry-java/pull/3659))
- - Also fix check whether Performance is enabled when making a sampling decision in the OpenTelemetry sampler
+ - Also fix check whether Performance is enabled when making a sampling decision in the OpenTelemetry sampler
- Sentry OpenTelemetry Java Agent now sets Instrumenter to SENTRY (used to be OTEL) ([#3697](https://github.com/getsentry/sentry-java/pull/3697))
- Set span origin in `ActivityLifecycleIntegration` on span options instead of after creating the span / transaction ([#3702](https://github.com/getsentry/sentry-java/pull/3702))
- - This allows spans to be filtered by span origin on creation
+ - This allows spans to be filtered by span origin on creation
- Honor ignored span origins in `SentryTracer.startChild` ([#3704](https://github.com/getsentry/sentry-java/pull/3704))
- Use span id of remote parent ([#3548](https://github.com/getsentry/sentry-java/pull/3548))
- - Traces were broken because on an incoming request, OtelSentrySpanProcessor did not set the parentSpanId on the span correctly. Traces were not referencing the actual parent span but some other (random) span ID which the server doesn't know.
+ - Traces were broken because on an incoming request, OtelSentrySpanProcessor did not set the parentSpanId on the span correctly. Traces were not referencing the actual parent span but some other (random) span ID which the server doesn't know.
- Attach active span to scope when using OpenTelemetry ([#3549](https://github.com/getsentry/sentry-java/pull/3549))
- - Errors weren't linked to traces correctly due to parts of the SDK not knowing the current span
+ - Errors weren't linked to traces correctly due to parts of the SDK not knowing the current span
- Record dropped spans in client report when sampling out OpenTelemetry spans ([#3552](https://github.com/getsentry/sentry-java/pull/3552))
- Retrieve the correct current span from `Scope`/`Scopes` when using OpenTelemetry ([#3554](https://github.com/getsentry/sentry-java/pull/3554))
- Support spans that are split into multiple batches ([#3539](https://github.com/getsentry/sentry-java/pull/3539))
- - When spans belonging to a single transaction were split into multiple batches for SpanExporter, we did not add all spans because the isSpanTooOld check wasn't inverted.
+ - When spans belonging to a single transaction were split into multiple batches for SpanExporter, we did not add all spans because the isSpanTooOld check wasn't inverted.
- Partially fix bootstrap class loading ([#3543](https://github.com/getsentry/sentry-java/pull/3543))
- - There was a problem with two separate Sentry `Scopes` being active inside each OpenTelemetry `Context` due to using context keys from more than one class loader.
+ - There was a problem with two separate Sentry `Scopes` being active inside each OpenTelemetry `Context` due to using context keys from more than one class loader.
- The Spring Boot 3 WebFlux sample now uses our GraphQL v22 integration ([#3828](https://github.com/getsentry/sentry-java/pull/3828))
- Do not ignore certain span origins for OpenTelemetry without agent ([#3856](https://github.com/getsentry/sentry-java/pull/3856))
- `span.startChild` now uses `.makeCurrent()` by default ([#3544](https://github.com/getsentry/sentry-java/pull/3544))
- - This caused an issue where the span tree wasn't correct because some spans were not added to their direct parent
+ - This caused an issue where the span tree wasn't correct because some spans were not added to their direct parent
- Do not set the exception group marker when there is a suppressed exception ([#4056](https://github.com/getsentry/sentry-java/pull/4056))
- - Due to how grouping works in Sentry currently sometimes the suppressed exception is treated as the main exception. This change ensures we keep using the main exception and not change how grouping works.
- - As a consequence the list of exceptions in the group on top of an issue is no longer shown in Sentry UI.
- - We are planning to improve this in the future but opted for this fix first.
+ - Due to how grouping works in Sentry currently sometimes the suppressed exception is treated as the main exception. This change ensures we keep using the main exception and not change how grouping works.
+ - As a consequence the list of exceptions in the group on top of an issue is no longer shown in Sentry UI.
+ - We are planning to improve this in the future but opted for this fix first.
### Dependencies
- Bump Native SDK from v0.7.0 to v0.7.17 ([#3441](https://github.com/getsentry/sentry-java/pull/3189)) ([#3851](https://github.com/getsentry/sentry-java/pull/3851)) ([#3914](https://github.com/getsentry/sentry-java/pull/3914)) ([#4003](https://github.com/getsentry/sentry-java/pull/4003))
- - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0717)
- - [diff](https://github.com/getsentry/sentry-native/compare/0.7.0...0.7.17)
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0717)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.7.0...0.7.17)
- Bump OpenTelemetry to 1.44.1, OpenTelemetry Java Agent to 2.10.0 and Semantic Conventions to 1.28.0 ([#3668](https://github.com/getsentry/sentry-java/pull/3668)) ([#3935](https://github.com/getsentry/sentry-java/pull/3935))
### Migration Guide / Deprecations
@@ -1142,10 +2226,10 @@ These changes have been made during development of `8.0.0`. You may skip this se
Please take a look at [our migration guide in docs](https://docs.sentry.io/platforms/java/migration/7.x-to-8.0).
- `Hub` has been deprecated, we're replacing the following:
- - `IHub` has been replaced by `IScopes`, however you should be able to simply pass `IHub` instances to code expecting `IScopes`, allowing for an easier migration.
- - `HubAdapter.getInstance()` has been replaced by `ScopesAdapter.getInstance()`
- - The `.clone()` method on `IHub`/`IScopes` has been deprecated, please use `.pushScope()` or `.pushIsolationScope()` instead
- - Some internal methods like `.getCurrentHub()` and `.setCurrentHub()` have also been replaced.
+ - `IHub` has been replaced by `IScopes`, however you should be able to simply pass `IHub` instances to code expecting `IScopes`, allowing for an easier migration.
+ - `HubAdapter.getInstance()` has been replaced by `ScopesAdapter.getInstance()`
+ - The `.clone()` method on `IHub`/`IScopes` has been deprecated, please use `.pushScope()` or `.pushIsolationScope()` instead
+ - Some internal methods like `.getCurrentHub()` and `.setCurrentHub()` have also been replaced.
- `Sentry.popScope` has been replaced by calling `.close()` on the token returned by `Sentry.pushScope()` and `Sentry.pushIsolationScope()`. The token can also be used in a `try` block like this:
```
@@ -1156,28 +2240,27 @@ try (final @NotNull ISentryLifecycleToken ignored = Sentry.pushScope()) {
as well as:
-
```
try (final @NotNull ISentryLifecycleToken ignored = Sentry.pushIsolationScope()) {
// this block has its separate isolation scope
}
```
+
- Classes used by our previous OpenTelemetry integration have been deprecated (`SentrySpanProcessor`, `SentryPropagator`, `OpenTelemetryLinkErrorEventProcessor`). Please take a look at [docs](https://docs.sentry.io/platforms/java/tracing/instrumentation/opentelemetry/) on how to setup OpenTelemetry in v8.
You may also use `LifecycleHelper.close(token)`, e.g. in case you need to pass the token around for closing later.
-
### Changes from `rc.4`
If you have been using `8.0.0-rc.4` of the Java SDK, here's the new changes that have been included in the `8.0.0` release:
- Make `SentryClient` constructor public ([#4045](https://github.com/getsentry/sentry-java/pull/4045))
- The user ip-address is now only set to `"{{auto}}"` if sendDefaultPii is enabled ([#4072](https://github.com/getsentry/sentry-java/pull/4072))
- - This change gives you control over IP address collection directly on the client
+ - This change gives you control over IP address collection directly on the client
- Do not set the exception group marker when there is a suppressed exception ([#4056](https://github.com/getsentry/sentry-java/pull/4056))
- - Due to how grouping works in Sentry currently sometimes the suppressed exception is treated as the main exception. This change ensures we keep using the main exception and not change how grouping works.
- - As a consequence the list of exceptions in the group on top of an issue is no longer shown in Sentry UI.
- - We are planning to improve this in the future but opted for this fix first.
+ - Due to how grouping works in Sentry currently sometimes the suppressed exception is treated as the main exception. This change ensures we keep using the main exception and not change how grouping works.
+ - As a consequence the list of exceptions in the group on top of an issue is no longer shown in Sentry UI.
+ - We are planning to improve this in the future but opted for this fix first.
- Fix swallow NDK loadLibrary errors ([#4082](https://github.com/getsentry/sentry-java/pull/4082))
## 7.22.6
@@ -1188,7 +2271,7 @@ If you have been using `8.0.0-rc.4` of the Java SDK, here's the new changes that
- Improve low memory breadcrumb capturing ([#4325](https://github.com/getsentry/sentry-java/pull/4325))
- Make `SystemEventsBreadcrumbsIntegration` faster ([#4330](https://github.com/getsentry/sentry-java/pull/4330))
- Fix unregister `SystemEventsBroadcastReceiver` when entering background ([#4338](https://github.com/getsentry/sentry-java/pull/4338))
- - This should reduce ANRs seen with this class in the stack trace for Android 14 and above
+ - This should reduce ANRs seen with this class in the stack trace for Android 14 and above
- Pre-load modules on a background thread upon SDK init ([#4348](https://github.com/getsentry/sentry-java/pull/4348))
- Session Replay: Fix inconsistent `segment_id` ([#4471](https://github.com/getsentry/sentry-java/pull/4471))
- Session Replay: Do not capture current replay for cached events from the past ([#4474](https://github.com/getsentry/sentry-java/pull/4474))
@@ -1236,11 +2319,11 @@ If you have been using `8.0.0-rc.4` of the Java SDK, here's the new changes that
### Fixes
- Session Replay: Fix various crashes and issues ([#4135](https://github.com/getsentry/sentry-java/pull/4135))
- - Fix `FileNotFoundException` when trying to read/write `.ongoing_segment` file
- - Fix `IllegalStateException` when registering `onDrawListener`
- - Fix SIGABRT native crashes on Motorola devices when encoding a video
+ - Fix `FileNotFoundException` when trying to read/write `.ongoing_segment` file
+ - Fix `IllegalStateException` when registering `onDrawListener`
+ - Fix SIGABRT native crashes on Motorola devices when encoding a video
- (Jetpack Compose) Modifier.sentryTag now uses Modifier.Node ([#4029](https://github.com/getsentry/sentry-java/pull/4029))
- - This allows Composables that use this modifier to be skippable
+ - This allows Composables that use this modifier to be skippable
## 7.21.0
@@ -1254,7 +2337,7 @@ If you have been using `8.0.0-rc.4` of the Java SDK, here's the new changes that
### Behavioural Changes
- (changed in [7.20.1](https://github.com/getsentry/sentry-java/releases/tag/7.20.1)) The user ip-address is now only set to `"{{auto}}"` if sendDefaultPii is enabled ([#4071](https://github.com/getsentry/sentry-java/pull/4071))
- - This change gives you control over IP address collection directly on the client
+ - This change gives you control over IP address collection directly on the client
- Reduce the number of broadcasts the SDK is subscribed for ([#4052](https://github.com/getsentry/sentry-java/pull/4052))
- Drop `TempSensorBreadcrumbsIntegration`
- Drop `PhoneStateBreadcrumbsIntegration`
@@ -1303,7 +2386,7 @@ If you would like to keep some of the default broadcast events as breadcrumbs, c
### Behavioural Changes
- The user ip-address is now only set to `"{{auto}}"` if sendDefaultPii is enabled ([#4071](https://github.com/getsentry/sentry-java/pull/4071))
- - This change gives you control over IP address collection directly on the client
+ - This change gives you control over IP address collection directly on the client
## 7.20.0
@@ -1313,23 +2396,23 @@ If you would like to keep some of the default broadcast events as breadcrumbs, c
To enable Replay use the `sessionReplay.sessionSampleRate` or `sessionReplay.onErrorSampleRate` options.
- ```kotlin
- import io.sentry.SentryReplayOptions
- import io.sentry.android.core.SentryAndroid
+```kotlin
+import io.sentry.SentryReplayOptions
+import io.sentry.android.core.SentryAndroid
- SentryAndroid.init(context) { options ->
-
- options.sessionReplay.sessionSampleRate = 1.0
- options.sessionReplay.onErrorSampleRate = 1.0
-
- // To change default redaction behavior (defaults to true)
- options.sessionReplay.redactAllImages = true
- options.sessionReplay.redactAllText = true
-
- // To change quality of the recording (defaults to MEDIUM)
- options.sessionReplay.quality = SentryReplayOptions.SentryReplayQuality.MEDIUM // (LOW|MEDIUM|HIGH)
- }
- ```
+SentryAndroid.init(context) { options ->
+
+ options.sessionReplay.sessionSampleRate = 1.0
+ options.sessionReplay.onErrorSampleRate = 1.0
+
+ // To change default redaction behavior (defaults to true)
+ options.sessionReplay.redactAllImages = true
+ options.sessionReplay.redactAllText = true
+
+ // To change quality of the recording (defaults to MEDIUM)
+ options.sessionReplay.quality = SentryReplayOptions.SentryReplayQuality.MEDIUM // (LOW|MEDIUM|HIGH)
+}
+```
### Fixes
@@ -1363,16 +2446,16 @@ To enable Replay use the `sessionReplay.sessionSampleRate` or `sessionReplay.onE
### Fixes
- Session Replay: fix various crashes and issues ([#3970](https://github.com/getsentry/sentry-java/pull/3970))
- - Fix `IndexOutOfBoundsException` when tracking window changes
- - Fix `IllegalStateException` when adding/removing draw listener for a dead view
- - Fix `ConcurrentModificationException` when registering window listeners and stopping `WindowRecorder`/`GestureRecorder`
+ - Fix `IndexOutOfBoundsException` when tracking window changes
+ - Fix `IllegalStateException` when adding/removing draw listener for a dead view
+ - Fix `ConcurrentModificationException` when registering window listeners and stopping `WindowRecorder`/`GestureRecorder`
- Add support for setting sentry-native handler_strategy ([#3671](https://github.com/getsentry/sentry-java/pull/3671))
### Dependencies
- Bump Native SDK from v0.7.8 to v0.7.16 ([#3671](https://github.com/getsentry/sentry-java/pull/3671))
- - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0716)
- - [diff](https://github.com/getsentry/sentry-native/compare/0.7.8...0.7.16)
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#0716)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.7.8...0.7.16)
## 7.18.1
@@ -1385,7 +2468,7 @@ To enable Replay use the `sessionReplay.sessionSampleRate` or `sessionReplay.onE
### Features
- Android 15: Add support for 16KB page sizes ([#3620](https://github.com/getsentry/sentry-java/pull/3620))
- - See https://developer.android.com/guide/practices/page-sizes for more details
+ - See https://developer.android.com/guide/practices/page-sizes for more details
- Session Replay: Add `beforeSendReplay` callback ([#3855](https://github.com/getsentry/sentry-java/pull/3855))
- Session Replay: Add support for masking/unmasking view containers ([#3881](https://github.com/getsentry/sentry-java/pull/3881))
@@ -1394,14 +2477,14 @@ To enable Replay use the `sessionReplay.sessionSampleRate` or `sessionReplay.onE
- Avoid collecting normal frames ([#3782](https://github.com/getsentry/sentry-java/pull/3782))
- Ensure android initialization process continues even if options configuration block throws an exception ([#3887](https://github.com/getsentry/sentry-java/pull/3887))
- Do not report parsing ANR error when there are no threads ([#3888](https://github.com/getsentry/sentry-java/pull/3888))
- - This should significantly reduce the number of events with message "Sentry Android SDK failed to parse system thread dump..." reported
+ - This should significantly reduce the number of events with message "Sentry Android SDK failed to parse system thread dump..." reported
- Session Replay: Disable replay in session mode when rate limit is active ([#3854](https://github.com/getsentry/sentry-java/pull/3854))
### Dependencies
- Bump Native SDK from v0.7.2 to v0.7.8 ([#3620](https://github.com/getsentry/sentry-java/pull/3620))
- - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#078)
- - [diff](https://github.com/getsentry/sentry-native/compare/0.7.2...0.7.8)
+ - [changelog](https://github.com/getsentry/sentry-native/blob/master/CHANGELOG.md#078)
+ - [diff](https://github.com/getsentry/sentry-native/compare/0.7.2...0.7.8)
## 7.17.0
@@ -1415,8 +2498,8 @@ To enable Replay use the `sessionReplay.sessionSampleRate` or `sessionReplay.onE
- Using MaxBreadcrumb with value 0 no longer crashes. ([#3836](https://github.com/getsentry/sentry-java/pull/3836))
- Accept manifest integer values when requiring floating values ([#3823](https://github.com/getsentry/sentry-java/pull/3823))
- Fix standalone tomcat jndi issue ([#3873](https://github.com/getsentry/sentry-java/pull/3873))
- - Using Sentry Spring Boot on a standalone tomcat caused the following error:
- - Failed to bind properties under 'sentry.parsed-dsn' to io.sentry.Dsn
+ - Using Sentry Spring Boot on a standalone tomcat caused the following error:
+ - Failed to bind properties under 'sentry.parsed-dsn' to io.sentry.Dsn
## 7.16.0
@@ -1440,7 +2523,7 @@ To enable Replay use the `sessionReplay.sessionSampleRate` or `sessionReplay.onE
### Breaking changes
-- The method `addIntegrationToSdkVersion(Ljava/lang/Class;)V` has been removed from the core (`io.sentry:sentry`) package. Please make sure all of the packages (e.g. `io.sentry:sentry-android-core`, `io.sentry:sentry-android-fragment`, `io.sentry:sentry-okhttp` and others) are all aligned and using the same version to prevent the `NoSuchMethodError` exception.
+- The method `addIntegrationToSdkVersion(Ljava/lang/Class;)V` has been removed from the core (`io.sentry:sentry`) package. Please make sure all of the packages (e.g. `io.sentry:sentry-android-core`, `io.sentry:sentry-android-fragment`, `io.sentry:sentry-okhttp` and others) are all aligned and using the same version to prevent the `NoSuchMethodError` exception.
## 7.16.0-alpha.1
@@ -1467,12 +2550,12 @@ To enable Replay use the `sessionReplay.sessionSampleRate` or `sessionReplay.onE
- Add support for `feedback` envelope header item type ([#3687](https://github.com/getsentry/sentry-java/pull/3687))
- Add breadcrumb.origin field ([#3727](https://github.com/getsentry/sentry-java/pull/3727))
- Session Replay: Add options to selectively mask/unmask views captured in replay. The following options are available: ([#3689](https://github.com/getsentry/sentry-java/pull/3689))
- - `android:tag="sentry-mask|sentry-unmask"` in XML or `view.setTag("sentry-mask|sentry-unmask")` in code tags
- - if you already have a tag set for a view, you can set a tag by id: ` ` in XML or `view.setTag(io.sentry.android.replay.R.id.sentry_privacy, "mask|unmask")` in code
- - `view.sentryReplayMask()` or `view.sentryReplayUnmask()` extension functions
- - mask/unmask `View`s of a certain type by adding fully-qualified classname to one of the lists `options.experimental.sessionReplay.addMaskViewClass()` or `options.experimental.sessionReplay.addUnmaskViewClass()`. Note, that all of the view subclasses/subtypes will be masked/unmasked as well
- - For example, (this is already a default behavior) to mask all `TextView`s and their subclasses (`RadioButton`, `EditText`, etc.): `options.experimental.sessionReplay.addMaskViewClass("android.widget.TextView")`
- - If you're using code obfuscation, adjust your proguard-rules accordingly, so your custom view class name is not minified
+ - `android:tag="sentry-mask|sentry-unmask"` in XML or `view.setTag("sentry-mask|sentry-unmask")` in code tags
+ - if you already have a tag set for a view, you can set a tag by id: ` ` in XML or `view.setTag(io.sentry.android.replay.R.id.sentry_privacy, "mask|unmask")` in code
+ - `view.sentryReplayMask()` or `view.sentryReplayUnmask()` extension functions
+ - mask/unmask `View`s of a certain type by adding fully-qualified classname to one of the lists `options.experimental.sessionReplay.addMaskViewClass()` or `options.experimental.sessionReplay.addUnmaskViewClass()`. Note, that all of the view subclasses/subtypes will be masked/unmasked as well
+ - For example, (this is already a default behavior) to mask all `TextView`s and their subclasses (`RadioButton`, `EditText`, etc.): `options.experimental.sessionReplay.addMaskViewClass("android.widget.TextView")`
+ - If you're using code obfuscation, adjust your proguard-rules accordingly, so your custom view class name is not minified
- Session Replay: Support Jetpack Compose masking ([#3739](https://github.com/getsentry/sentry-java/pull/3739))
- To selectively mask/unmask @Composables, use `Modifier.sentryReplayMask()` and `Modifier.sentryReplayUnmask()` modifiers
- Session Replay: Mask `WebView`, `VideoView` and `androidx.media3.ui.PlayerView` by default ([#3775](https://github.com/getsentry/sentry-java/pull/3775))
@@ -1486,7 +2569,7 @@ To enable Replay use the `sessionReplay.sessionSampleRate` or `sessionReplay.onE
- Fix potential ANRs due to default integrations ([#3778](https://github.com/getsentry/sentry-java/pull/3778))
- Lazily initialize heavy `SentryOptions` members to avoid ANRs on app start ([#3749](https://github.com/getsentry/sentry-java/pull/3749))
-*Breaking changes*:
+_Breaking changes_:
- `options.experimental.sessionReplay.errorSampleRate` was renamed to `options.experimental.sessionReplay.onErrorSampleRate` ([#3637](https://github.com/getsentry/sentry-java/pull/3637))
- Manifest option `io.sentry.session-replay.error-sample-rate` was renamed to `io.sentry.session-replay.on-error-sample-rate` ([#3637](https://github.com/getsentry/sentry-java/pull/3637))
@@ -1556,15 +2639,15 @@ To enable Replay use the `sessionReplay.sessionSampleRate` or `sessionReplay.onE
import io.sentry.android.core.SentryAndroid
SentryAndroid.init(context) { options ->
-
+
// Currently under experimental options:
options.experimental.sessionReplay.sessionSampleRate = 1.0
options.experimental.sessionReplay.errorSampleRate = 1.0
-
+
// To change default redaction behavior (defaults to true)
options.experimental.sessionReplay.redactAllImages = true
options.experimental.sessionReplay.redactAllText = true
-
+
// To change quality of the recording (defaults to MEDIUM)
options.experimental.sessionReplay.quality = SentryReplayOptions.SentryReplayQuality.MEDIUM // (LOW|MEDIUM|HIGH)
}
@@ -1653,7 +2736,7 @@ To enable Replay use the `sessionReplay.sessionSampleRate` or `sessionReplay.onE
### Features
-- Experimental: Add support for Sentry Developer Metrics ([#3205](https://github.com/getsentry/sentry-java/pull/3205), [#3238](https://github.com/getsentry/sentry-java/pull/3238), [#3248](https://github.com/getsentry/sentry-java/pull/3248), [#3250](https://github.com/getsentry/sentry-java/pull/3250))
+- Experimental: Add support for Sentry Developer Metrics ([#3205](https://github.com/getsentry/sentry-java/pull/3205), [#3238](https://github.com/getsentry/sentry-java/pull/3238), [#3248](https://github.com/getsentry/sentry-java/pull/3248), [#3250](https://github.com/getsentry/sentry-java/pull/3250))
Use the Metrics API to track processing time, download sizes, user signups, and conversion rates and correlate them back to tracing data in order to get deeper insights and solve issues faster. Our API supports counters, distributions, sets, gauges and timers, and it's easy to get started:
```kotlin
Sentry.metrics()
@@ -1697,8 +2780,8 @@ To enable Replay use the `sessionReplay.sessionSampleRate` or `sessionReplay.onE
- (perf-v2): Calculate frame delay on a span level ([#3197](https://github.com/getsentry/sentry-java/pull/3197))
- Resolve spring properties in @SentryCheckIn annotation ([#3194](https://github.com/getsentry/sentry-java/pull/3194))
- Experimental: Add Spotlight integration ([#3166](https://github.com/getsentry/sentry-java/pull/3166))
- - For more details about Spotlight head over to https://spotlightjs.com/
- - Set `options.isEnableSpotlight = true` to enable Spotlight
+ - For more details about Spotlight head over to https://spotlightjs.com/
+ - Set `options.isEnableSpotlight = true` to enable Spotlight
### Fixes
@@ -1712,12 +2795,12 @@ To enable Replay use the `sessionReplay.sessionSampleRate` or `sessionReplay.onE
### Features
- Added App Start profiling
- - This depends on the new option `io.sentry.profiling.enable-app-start`, other than the already existing `io.sentry.traces.profiling.sample-rate`.
- - Sampler functions can check the new `isForNextAppStart` flag, to adjust startup profiling sampling programmatically.
- Relevant PRs:
- - Decouple Profiler from Transaction ([#3101](https://github.com/getsentry/sentry-java/pull/3101))
- - Add options and sampling logic ([#3121](https://github.com/getsentry/sentry-java/pull/3121))
- - Add ContentProvider and start profile ([#3128](https://github.com/getsentry/sentry-java/pull/3128))
+ - This depends on the new option `io.sentry.profiling.enable-app-start`, other than the already existing `io.sentry.traces.profiling.sample-rate`.
+ - Sampler functions can check the new `isForNextAppStart` flag, to adjust startup profiling sampling programmatically.
+ Relevant PRs:
+ - Decouple Profiler from Transaction ([#3101](https://github.com/getsentry/sentry-java/pull/3101))
+ - Add options and sampling logic ([#3121](https://github.com/getsentry/sentry-java/pull/3121))
+ - Add ContentProvider and start profile ([#3128](https://github.com/getsentry/sentry-java/pull/3128))
- Extend internal performance collector APIs ([#3102](https://github.com/getsentry/sentry-java/pull/3102))
- Collect slow and frozen frames for spans using `OnFrameMetricsAvailableListener` ([#3111](https://github.com/getsentry/sentry-java/pull/3111))
- Interpolate total frame count to match span duration ([#3158](https://github.com/getsentry/sentry-java/pull/3158))
@@ -1799,8 +2882,9 @@ To enable Replay use the `sessionReplay.sessionSampleRate` or `sessionReplay.onE
## 7.0.0
Version 7 of the Sentry Android/Java SDK brings a variety of features and fixes. The most notable changes are:
+
- Bumping `minSdk` level to 19 (Android 4.4)
-- The SDK will now listen to connectivity changes and try to re-upload cached events when internet connection is re-established additionally to uploading events on app restart
+- The SDK will now listen to connectivity changes and try to re-upload cached events when internet connection is re-established additionally to uploading events on app restart
- `Sentry.getSpan` now returns the root transaction, which should improve the span hierarchy and make it leaner
- Multiple improvements to reduce probability of the SDK causing ANRs
- New `sentry-okhttp` artifact is unbundled from Android and can be used in pure JVM-only apps
@@ -1834,8 +2918,8 @@ Similarly, if you have a Sentry SDK (e.g. `sentry-android-core`) dependency on o
- `SentryOkHttpUtils` was removed from public API as it's been exposed by mistake ([#3005](https://github.com/getsentry/sentry-java/pull/3005))
- `Scope` now implements the `IScope` interface, therefore some methods like `ScopeCallback.run` accept `IScope` now ([#3066](https://github.com/getsentry/sentry-java/pull/3066))
- Cleanup `startTransaction` overloads ([#2964](https://github.com/getsentry/sentry-java/pull/2964))
- - We have reduced the number of overloads by allowing to pass in a `TransactionOptions` object instead of having separate parameters for certain options
- - `TransactionOptions` has defaults set and can be customized, for example:
+ - We have reduced the number of overloads by allowing to pass in a `TransactionOptions` object instead of having separate parameters for certain options
+ - `TransactionOptions` has defaults set and can be customized, for example:
```kotlin
// old
@@ -1848,14 +2932,14 @@ val transaction = Sentry.startTransaction("name", "op", TransactionOptions().app
- Android only: `Sentry.getSpan()` returns the root span/transaction instead of the latest span ([#2855](https://github.com/getsentry/sentry-java/pull/2855))
- Capture failed HTTP and GraphQL (Apollo) requests by default ([#2794](https://github.com/getsentry/sentry-java/pull/2794))
- - This can increase your event consumption and may affect your quota, because we will report failed network requests as Sentry events by default, if you're using the `sentry-android-okhttp` or `sentry-apollo-3` integrations. You can customize what errors you want/don't want to have reported for [OkHttp](https://docs.sentry.io/platforms/android/integrations/okhttp#http-client-errors) and [Apollo3](https://docs.sentry.io/platforms/android/integrations/apollo3#graphql-client-errors) respectively.
+ - This can increase your event consumption and may affect your quota, because we will report failed network requests as Sentry events by default, if you're using the `sentry-android-okhttp` or `sentry-apollo-3` integrations. You can customize what errors you want/don't want to have reported for [OkHttp](https://docs.sentry.io/platforms/android/integrations/okhttp#http-client-errors) and [Apollo3](https://docs.sentry.io/platforms/android/integrations/apollo3#graphql-client-errors) respectively.
- Measure AppStart time till First Draw instead of `onResume` ([#2851](https://github.com/getsentry/sentry-java/pull/2851))
- Automatic user interaction tracking: every click now starts a new automatic transaction ([#2891](https://github.com/getsentry/sentry-java/pull/2891))
- - Previously performing a click on the same UI widget twice would keep the existing transaction running, the new behavior now better aligns with other SDKs
+ - Previously performing a click on the same UI widget twice would keep the existing transaction running, the new behavior now better aligns with other SDKs
- Add deadline timeout for automatic transactions ([#2865](https://github.com/getsentry/sentry-java/pull/2865))
- - This affects all automatically generated transactions on Android (UI, clicks), the default timeout is 30s, meaning the automatic transaction will be force-finished with status `deadline_exceeded` when reaching the deadline
+ - This affects all automatically generated transactions on Android (UI, clicks), the default timeout is 30s, meaning the automatic transaction will be force-finished with status `deadline_exceeded` when reaching the deadline
- Set ip_address to {{auto}} by default, even if sendDefaultPII is disabled ([#2860](https://github.com/getsentry/sentry-java/pull/2860))
- - Instead use the "Prevent Storing of IP Addresses" option in the "Security & Privacy" project settings on sentry.io
+ - Instead use the "Prevent Storing of IP Addresses" option in the "Security & Privacy" project settings on sentry.io
- Raw logback message and parameters are now guarded by `sendDefaultPii` if an `encoder` has been configured ([#2976](https://github.com/getsentry/sentry-java/pull/2976))
- The `maxSpans` setting (defaults to 1000) is enforced for nested child spans which means a single transaction can have `maxSpans` number of children (nested or not) at most ([#3065](https://github.com/getsentry/sentry-java/pull/3065))
- The `ScopeCallback` in `withScope` is now always executed ([#3066](https://github.com/getsentry/sentry-java/pull/3066))
@@ -1869,8 +2953,8 @@ val transaction = Sentry.startTransaction("name", "op", TransactionOptions().app
### Features
- Observe network state to upload any unsent envelopes ([#2910](https://github.com/getsentry/sentry-java/pull/2910))
- - Android: it works out-of-the-box as part of the default `SendCachedEnvelopeIntegration`
- - JVM: you'd have to install `SendCachedEnvelopeFireAndForgetIntegration` as mentioned in https://docs.sentry.io/platforms/java/configuration/#configuring-offline-caching and provide your own implementation of `IConnectionStatusProvider` via `SentryOptions`
+ - Android: it works out-of-the-box as part of the default `SendCachedEnvelopeIntegration`
+ - JVM: you'd have to install `SendCachedEnvelopeFireAndForgetIntegration` as mentioned in https://docs.sentry.io/platforms/java/configuration/#configuring-offline-caching and provide your own implementation of `IConnectionStatusProvider` via `SentryOptions`
- Add `sentry-okhttp` module to support instrumenting OkHttp in non-Android projects ([#3005](https://github.com/getsentry/sentry-java/pull/3005))
- Do not filter out Sentry SDK frames in case of uncaught exceptions ([#3021](https://github.com/getsentry/sentry-java/pull/3021))
- Do not try to send and drop cached envelopes when rate-limiting is active ([#2937](https://github.com/getsentry/sentry-java/pull/2937))
@@ -1878,16 +2962,16 @@ val transaction = Sentry.startTransaction("name", "op", TransactionOptions().app
### Fixes
- Use `getMyMemoryState()` instead of `getRunningAppProcesses()` to retrieve process importance ([#3004](https://github.com/getsentry/sentry-java/pull/3004))
- - This should prevent some app stores from flagging apps as violating their privacy
+ - This should prevent some app stores from flagging apps as violating their privacy
- Reduce flush timeout to 4s on Android to avoid ANRs ([#2858](https://github.com/getsentry/sentry-java/pull/2858))
- Reduce timeout of AsyncHttpTransport to avoid ANR ([#2879](https://github.com/getsentry/sentry-java/pull/2879))
- Do not overwrite UI transaction status if set by the user ([#2852](https://github.com/getsentry/sentry-java/pull/2852))
- Capture unfinished transaction on Scope with status `aborted` in case a crash happens ([#2938](https://github.com/getsentry/sentry-java/pull/2938))
- - This will fix the link between transactions and corresponding crashes, you'll be able to see them in a single trace
+ - This will fix the link between transactions and corresponding crashes, you'll be able to see them in a single trace
- Fix Coroutine Context Propagation using CopyableThreadContextElement ([#2838](https://github.com/getsentry/sentry-java/pull/2838))
- Fix don't overwrite the span status of unfinished spans ([#2859](https://github.com/getsentry/sentry-java/pull/2859))
- Migrate from `default` interface methods to proper implementations in each interface implementor ([#2847](https://github.com/getsentry/sentry-java/pull/2847))
- - This prevents issues when using the SDK on older AGP versions (< 4.x.x)
+ - This prevents issues when using the SDK on older AGP versions (< 4.x.x)
- Reduce main thread work on init ([#3036](https://github.com/getsentry/sentry-java/pull/3036))
- Move Integrations registration to background on init ([#3043](https://github.com/getsentry/sentry-java/pull/3043))
- Fix `SentryOkHttpInterceptor.BeforeSpanCallback` was not finishing span when it was dropped ([#2958](https://github.com/getsentry/sentry-java/pull/2958))
@@ -1906,7 +2990,7 @@ val transaction = Sentry.startTransaction("name", "op", TransactionOptions().app
### Fixes
-- Fix SIGSEV, SIGABRT and SIGBUS crashes happening after/around the August Google Play System update, see [#2955](https://github.com/getsentry/sentry-java/issues/2955) for more details (fix provided by Native SDK bump)
+- Fix SIGSEV, SIGABRT and SIGBUS crashes happening after/around the August Google Play System update, see [#2955](https://github.com/getsentry/sentry-java/issues/2955) for more details (fix provided by Native SDK bump)
- Ensure DSN uses http/https protocol ([#3044](https://github.com/getsentry/sentry-java/pull/3044))
### Dependencies
@@ -1919,7 +3003,7 @@ val transaction = Sentry.startTransaction("name", "op", TransactionOptions().app
### Fixes
-- Fix SIGSEV, SIGABRT and SIGBUS crashes happening after/around the August Google Play System update, see [#2955](https://github.com/getsentry/sentry-java/issues/2955) for more details (fix provided by Native SDK bump)
+- Fix SIGSEV, SIGABRT and SIGBUS crashes happening after/around the August Google Play System update, see [#2955](https://github.com/getsentry/sentry-java/issues/2955) for more details (fix provided by Native SDK bump)
### Dependencies
@@ -2031,15 +3115,15 @@ val transaction = Sentry.startTransaction("name", "op", TransactionOptions().app
- Add HTTP response code to Spring WebFlux transactions ([#2870](https://github.com/getsentry/sentry-java/pull/2870))
- Add `sampled` to Dynamic Sampling Context ([#2869](https://github.com/getsentry/sentry-java/pull/2869))
- Improve server side GraphQL support for spring-graphql and Nextflix DGS ([#2856](https://github.com/getsentry/sentry-java/pull/2856))
- - If you have already been using `SentryDataFetcherExceptionHandler` that still works but has been deprecated. Please use `SentryGenericDataFetcherExceptionHandler` combined with `SentryInstrumentation` instead for better error reporting.
- - More exceptions and errors caught and reported to Sentry by also looking at the `ExecutionResult` (more specifically its `errors`)
- - You may want to filter out certain errors, please see [docs on filtering](https://docs.sentry.io/platforms/java/configuration/filtering/)
- - More details for Sentry events: query, variables and response (where possible)
- - Breadcrumbs for operation (query, mutation, subscription), data fetchers and data loaders (Spring only)
- - Better hub propagation by using `GraphQLContext`
+ - If you have already been using `SentryDataFetcherExceptionHandler` that still works but has been deprecated. Please use `SentryGenericDataFetcherExceptionHandler` combined with `SentryInstrumentation` instead for better error reporting.
+ - More exceptions and errors caught and reported to Sentry by also looking at the `ExecutionResult` (more specifically its `errors`)
+ - You may want to filter out certain errors, please see [docs on filtering](https://docs.sentry.io/platforms/java/configuration/filtering/)
+ - More details for Sentry events: query, variables and response (where possible)
+ - Breadcrumbs for operation (query, mutation, subscription), data fetchers and data loaders (Spring only)
+ - Better hub propagation by using `GraphQLContext`
- Add autoconfigure modules for Spring Boot called `sentry-spring-boot` and `sentry-spring-boot-jakarta` ([#2880](https://github.com/getsentry/sentry-java/pull/2880))
- The autoconfigure modules `sentry-spring-boot` and `sentry-spring-boot-jakarta` have a `compileOnly` dependency on `spring-boot-starter` which is needed for our auto installation in [sentry-android-gradle-plugin](https://github.com/getsentry/sentry-android-gradle-plugin)
- - The starter modules `sentry-spring-boot-starter` and `sentry-spring-boot-starter-jakarta` now bring `spring-boot-starter` as a dependency
+ - The starter modules `sentry-spring-boot-starter` and `sentry-spring-boot-starter-jakarta` now bring `spring-boot-starter` as a dependency
- You can now disable Sentry by setting the `enabled` option to `false` ([#2840](https://github.com/getsentry/sentry-java/pull/2840))
### Fixes
@@ -2062,6 +3146,7 @@ val transaction = Sentry.startTransaction("name", "op", TransactionOptions().app
## 6.26.0
### Features
+
- (Internal) Extend APIs for hybrid SDKs ([#2814](https://github.com/getsentry/sentry-java/pull/2814), [#2846](https://github.com/getsentry/sentry-java/pull/2846))
### Fixes
@@ -2122,13 +3207,13 @@ val transaction = Sentry.startTransaction("name", "op", TransactionOptions().app
- Add debouncing mechanism and before-capture callbacks for screenshots and view hierarchies ([#2773](https://github.com/getsentry/sentry-java/pull/2773))
- Improve ANRv2 implementation ([#2792](https://github.com/getsentry/sentry-java/pull/2792))
- Add a proguard rule to keep `ApplicationNotResponding` class from obfuscation
- - Add a new option `setReportHistoricalAnrs`; when enabled, it will report all of the ANRs from the [getHistoricalExitReasons](https://developer.android.com/reference/android/app/ActivityManager?hl=en#getHistoricalProcessExitReasons(java.lang.String,%20int,%20int)) list.
- By default, the SDK only reports and enriches the latest ANR and only this one counts towards ANR rate.
- Worth noting that this option is mainly useful when updating the SDK to the version where ANRv2 has been introduced, to report all ANRs happened prior to the SDK update. After that, the SDK will always pick up the latest ANR from the historical exit reasons list on next app restart, so there should be no historical ANRs to report.
- These ANRs are reported with the `HistoricalAppExitInfo` mechanism.
- - Add a new option `setAttachAnrThreadDump` to send ANR thread dump from the system as an attachment.
- This is only useful as additional information, because the SDK attempts to parse the thread dump into proper threads with stacktraces by default.
- - If [ApplicationExitInfo#getTraceInputStream](https://developer.android.com/reference/android/app/ApplicationExitInfo#getTraceInputStream()) returns null, the SDK no longer reports an ANR event, as these events are not very useful without it.
+ - Add a new option `setReportHistoricalAnrs`; when enabled, it will report all of the ANRs from the [getHistoricalExitReasons]() list.
+ By default, the SDK only reports and enriches the latest ANR and only this one counts towards ANR rate.
+ Worth noting that this option is mainly useful when updating the SDK to the version where ANRv2 has been introduced, to report all ANRs happened prior to the SDK update. After that, the SDK will always pick up the latest ANR from the historical exit reasons list on next app restart, so there should be no historical ANRs to report.
+ These ANRs are reported with the `HistoricalAppExitInfo` mechanism.
+ - Add a new option `setAttachAnrThreadDump` to send ANR thread dump from the system as an attachment.
+ This is only useful as additional information, because the SDK attempts to parse the thread dump into proper threads with stacktraces by default.
+ - If [ApplicationExitInfo#getTraceInputStream]() returns null, the SDK no longer reports an ANR event, as these events are not very useful without it.
- Enhance regex patterns for native stackframes
## 6.23.0
@@ -2144,7 +3229,7 @@ import io.sentry.apollo3.sentryTracing
val apolloClient = ApolloClient.Builder()
.serverUrl("https://example.com/graphql")
- .sentryTracing(captureFailedRequests = true)
+ .sentryTracing(captureFailedRequests = true)
.build()
```
@@ -2175,9 +3260,9 @@ val apolloClient = ApolloClient.Builder()
### Features
- Introduce new `sentry-android-sqlite` integration ([#2722](https://github.com/getsentry/sentry-java/pull/2722))
- - This integration replaces the old `androidx.sqlite` database instrumentation in the Sentry Android Gradle plugin
- - A new capability to manually instrument your `androidx.sqlite` databases.
- - You can wrap your custom `SupportSQLiteOpenHelper` instance into `SentrySupportSQLiteOpenHelper(myHelper)` if you're not using the Sentry Android Gradle plugin and still benefit from performance auto-instrumentation.
+ - This integration replaces the old `androidx.sqlite` database instrumentation in the Sentry Android Gradle plugin
+ - A new capability to manually instrument your `androidx.sqlite` databases.
+ - You can wrap your custom `SupportSQLiteOpenHelper` instance into `SentrySupportSQLiteOpenHelper(myHelper)` if you're not using the Sentry Android Gradle plugin and still benefit from performance auto-instrumentation.
- Add SentryWrapper for Callable and Supplier Interface ([#2720](https://github.com/getsentry/sentry-java/pull/2720))
- Load sentry-debug-meta.properties ([#2734](https://github.com/getsentry/sentry-java/pull/2734))
- This enables source context for Java
@@ -2200,15 +3285,15 @@ val apolloClient = ApolloClient.Builder()
- [View Hierarchy](https://docs.sentry.io/platforms/android/enriching-events/viewhierarchy/) support for Jetpack Compose screens
- Automatic breadcrumbs for [user interactions](https://docs.sentry.io/platforms/android/performance/instrumentation/automatic-instrumentation/#user-interaction-instrumentation)
- More granular http requests instrumentation with a new SentryOkHttpEventListener ([#2659](https://github.com/getsentry/sentry-java/pull/2659))
- - Create spans for time spent on:
- - Proxy selection
- - DNS resolution
- - HTTPS setup
- - Connection
- - Requesting headers
- - Receiving response
- - You can attach the event listener to your OkHttpClient through `client.eventListener(new SentryOkHttpEventListener()).addInterceptor(new SentryOkHttpInterceptor()).build();`
- - In case you already have an event listener you can use the SentryOkHttpEventListener as well through `client.eventListener(new SentryOkHttpEventListener(myListener)).addInterceptor(new SentryOkHttpInterceptor()).build();`
+ - Create spans for time spent on:
+ - Proxy selection
+ - DNS resolution
+ - HTTPS setup
+ - Connection
+ - Requesting headers
+ - Receiving response
+ - You can attach the event listener to your OkHttpClient through `client.eventListener(new SentryOkHttpEventListener()).addInterceptor(new SentryOkHttpInterceptor()).build();`
+ - In case you already have an event listener you can use the SentryOkHttpEventListener as well through `client.eventListener(new SentryOkHttpEventListener(myListener)).addInterceptor(new SentryOkHttpInterceptor()).build();`
- Add a new option to disable `RootChecker` ([#2735](https://github.com/getsentry/sentry-java/pull/2735))
### Fixes
@@ -2229,16 +3314,16 @@ val apolloClient = ApolloClient.Builder()
- Add Screenshot and ViewHierarchy to integrations list ([#2698](https://github.com/getsentry/sentry-java/pull/2698))
- New ANR detection based on [ApplicationExitInfo API](https://developer.android.com/reference/android/app/ApplicationExitInfo) ([#2697](https://github.com/getsentry/sentry-java/pull/2697))
- - This implementation completely replaces the old one (based on a watchdog) on devices running Android 11 and above:
- - New implementation provides more precise ANR events/ANR rate detection as well as system thread dump information. The new implementation reports ANRs exactly as Google Play Console, without producing false positives or missing important background ANR events.
- - New implementation reports ANR events with a new mechanism `mechanism:AppExitInfo`.
- - However, despite producing many false positives, the old implementation is capable of better enriching ANR errors (which is not available with the new implementation), for example:
- - Capturing screenshots at the time of ANR event;
- - Capturing transactions and profiling data corresponding to the ANR event;
- - Auxiliary information (such as current memory load) at the time of ANR event.
- - If you would like us to provide support for the old approach working alongside the new one on Android 11 and above (e.g. for raising events for slow code on main thread), consider upvoting [this issue](https://github.com/getsentry/sentry-java/issues/2693).
- - The old watchdog implementation will continue working for older API versions (Android < 11):
- - The old implementation reports ANR events with the existing mechanism `mechanism:ANR`.
+ - This implementation completely replaces the old one (based on a watchdog) on devices running Android 11 and above:
+ - New implementation provides more precise ANR events/ANR rate detection as well as system thread dump information. The new implementation reports ANRs exactly as Google Play Console, without producing false positives or missing important background ANR events.
+ - New implementation reports ANR events with a new mechanism `mechanism:AppExitInfo`.
+ - However, despite producing many false positives, the old implementation is capable of better enriching ANR errors (which is not available with the new implementation), for example:
+ - Capturing screenshots at the time of ANR event;
+ - Capturing transactions and profiling data corresponding to the ANR event;
+ - Auxiliary information (such as current memory load) at the time of ANR event.
+ - If you would like us to provide support for the old approach working alongside the new one on Android 11 and above (e.g. for raising events for slow code on main thread), consider upvoting [this issue](https://github.com/getsentry/sentry-java/issues/2693).
+ - The old watchdog implementation will continue working for older API versions (Android < 11):
+ - The old implementation reports ANR events with the existing mechanism `mechanism:ANR`.
- Open up `TransactionOptions`, `ITransaction` and `IHub` methods allowing consumers modify start/end timestamp of transactions and spans ([#2701](https://github.com/getsentry/sentry-java/pull/2701))
- Send source bundle IDs to Sentry to enable source context ([#2663](https://github.com/getsentry/sentry-java/pull/2663))
- For more information on how to enable source context, please refer to [#633](https://github.com/getsentry/sentry-java/issues/633#issuecomment-1465599120)
@@ -2273,7 +3358,7 @@ val apolloClient = ApolloClient.Builder()
- Attach Trace Context when an ANR is detected (ANRv1) ([#2583](https://github.com/getsentry/sentry-java/pull/2583))
- Make log4j2 integration compatible with log4j 3.0 ([#2634](https://github.com/getsentry/sentry-java/pull/2634))
- - Instead of relying on package scanning, we now use an annotation processor to generate `Log4j2Plugins.dat`
+ - Instead of relying on package scanning, we now use an annotation processor to generate `Log4j2Plugins.dat`
- Create `User` and `Breadcrumb` from map ([#2614](https://github.com/getsentry/sentry-java/pull/2614))
- Add `sent_at` to envelope header item ([#2638](https://github.com/getsentry/sentry-java/pull/2638))
@@ -2286,12 +3371,13 @@ val apolloClient = ApolloClient.Builder()
- Fix aar artifacts publishing for Maven ([#2641](https://github.com/getsentry/sentry-java/pull/2641))
### Dependencies
+
- Bump Kotlin compile version from v1.6.10 to 1.8.0 ([#2563](https://github.com/getsentry/sentry-java/pull/2563))
- Bump Compose compile version from v1.1.1 to v1.3.0 ([#2563](https://github.com/getsentry/sentry-java/pull/2563))
- Bump AGP version from v7.3.0 to v7.4.2 ([#2574](https://github.com/getsentry/sentry-java/pull/2574))
- Bump Gradle from v7.6.0 to v8.0.2 ([#2563](https://github.com/getsentry/sentry-java/pull/2563))
- - [changelog](https://github.com/gradle/gradle/blob/master/CHANGELOG.md#v802)
- - [diff](https://github.com/gradle/gradle/compare/v7.6.0...v8.0.2)
+ - [changelog](https://github.com/gradle/gradle/blob/master/CHANGELOG.md#v802)
+ - [diff](https://github.com/gradle/gradle/compare/v7.6.0...v8.0.2)
- Bump Gradle from v8.0.2 to v8.1.0 ([#2650](https://github.com/getsentry/sentry-java/pull/2650))
- [changelog](https://github.com/gradle/gradle/blob/master/CHANGELOG.md#v810)
- [diff](https://github.com/gradle/gradle/compare/v8.0.2...v8.1.0)
@@ -2300,7 +3386,7 @@ val apolloClient = ApolloClient.Builder()
### Features
-- Add `name` and `geo` to `User` ([#2556](https://github.com/getsentry/sentry-java/pull/2556))
+- Add `name` and `geo` to `User` ([#2556](https://github.com/getsentry/sentry-java/pull/2556))
- Add breadcrumbs on network changes ([#2608](https://github.com/getsentry/sentry-java/pull/2608))
- Add time-to-initial-display and time-to-full-display measurements to Activity transactions ([#2611](https://github.com/getsentry/sentry-java/pull/2611))
- Read integration list written by sentry gradle plugin from manifest ([#2598](https://github.com/getsentry/sentry-java/pull/2598))
@@ -2338,7 +3424,7 @@ val apolloClient = ApolloClient.Builder()
- Fix timestamps of slow and frozen frames for profiles ([#2584](https://github.com/getsentry/sentry-java/pull/2584))
- Deprecate reportFullDisplayed in favor of reportFullyDisplayed ([#2585](https://github.com/getsentry/sentry-java/pull/2585))
- Add mechanism for logging integrations and update spring mechanism types ([#2595](https://github.com/getsentry/sentry-java/pull/2595))
- - NOTE: If you're using these mechanism types (`HandlerExceptionResolver`, `SentryWebExceptionHandler`) in your dashboards please update them to use the new types.
+ - NOTE: If you're using these mechanism types (`HandlerExceptionResolver`, `SentryWebExceptionHandler`) in your dashboards please update them to use the new types.
- Filter out session cookies sent by Spring and Spring Boot integrations ([#2593](https://github.com/getsentry/sentry-java/pull/2593))
- We filter out some common cookies like JSESSIONID
- We also read the value from `server.servlet.session.cookie.name` and filter it out
@@ -2363,9 +3449,9 @@ val apolloClient = ApolloClient.Builder()
- Adjust time-to-full-display span if reportFullDisplayed is called too early ([#2550](https://github.com/getsentry/sentry-java/pull/2550))
- Add `enableTracing` option ([#2530](https://github.com/getsentry/sentry-java/pull/2530))
- - This change is backwards compatible. The default is `null` meaning existing behaviour remains unchanged (setting either `tracesSampleRate` or `tracesSampler` enables performance).
- - If set to `true`, performance is enabled, even if no `tracesSampleRate` or `tracesSampler` have been configured.
- - If set to `false` performance is disabled, regardless of `tracesSampleRate` and `tracesSampler` options.
+ - This change is backwards compatible. The default is `null` meaning existing behaviour remains unchanged (setting either `tracesSampleRate` or `tracesSampler` enables performance).
+ - If set to `true`, performance is enabled, even if no `tracesSampleRate` or `tracesSampler` have been configured.
+ - If set to `false` performance is disabled, regardless of `tracesSampleRate` and `tracesSampler` options.
- Detect dependencies by listing MANIFEST.MF files at runtime ([#2538](https://github.com/getsentry/sentry-java/pull/2538))
- Report integrations in use, report packages in use more consistently ([#2179](https://github.com/getsentry/sentry-java/pull/2179))
- Implement `ThreadLocalAccessor` for propagating Sentry hub with reactor / WebFlux ([#2570](https://github.com/getsentry/sentry-java/pull/2570))
@@ -2392,7 +3478,7 @@ val apolloClient = ApolloClient.Builder()
### Features
- Add time-to-full-display span to Activity auto-instrumentation ([#2432](https://github.com/getsentry/sentry-java/pull/2432))
-- Add `main` flag to threads and `in_foreground` flag for app contexts ([#2516](https://github.com/getsentry/sentry-java/pull/2516))
+- Add `main` flag to threads and `in_foreground` flag for app contexts ([#2516](https://github.com/getsentry/sentry-java/pull/2516))
### Fixes
@@ -2628,7 +3714,7 @@ val apolloClient = ApolloClient.Builder()
### Features
-- Server-Side Dynamic Sampling Context support ([#2226](https://github.com/getsentry/sentry-java/pull/2226))
+- Server-Side Dynamic Sampling Context support ([#2226](https://github.com/getsentry/sentry-java/pull/2226))
## 6.4.4
@@ -2689,7 +3775,6 @@ val apolloClient = ApolloClient.Builder()
- `attach-screenshot` set on Manual init. didn't work ([#2186](https://github.com/getsentry/sentry-java/pull/2186))
- Remove extra space from `spring.factories` causing issues in old versions of Spring Boot ([#2181](https://github.com/getsentry/sentry-java/pull/2181))
-
### Features
- Bump Native SDK to v0.4.18 ([#2154](https://github.com/getsentry/sentry-java/pull/2154))
@@ -2724,6 +3809,7 @@ val apolloClient = ApolloClient.Builder()
- Add sample rate to baggage as well as trace in envelope header and flatten user ([#2135](https://github.com/getsentry/sentry-java/pull/2135))
Breaking Changes:
+
- The boolean parameter `samplingDecision` in the `TransactionContext` constructor has been replaced with a `TracesSamplingDecision` object. Feel free to ignore the `@ApiStatus.Internal` in this case.
## 6.1.4
@@ -2787,19 +3873,19 @@ Breaking Changes:
- Add Android profiling traces ([#1897](https://github.com/getsentry/sentry-java/pull/1897)) ([#1959](https://github.com/getsentry/sentry-java/pull/1959)) and its tests ([#1949](https://github.com/getsentry/sentry-java/pull/1949))
- Enable enableScopeSync by default for Android ([#1928](https://github.com/getsentry/sentry-java/pull/1928))
- Feat: Vendor JSON ([#1554](https://github.com/getsentry/sentry-java/pull/1554))
- - Introduce `JsonSerializable` and `JsonDeserializer` interfaces for manual json
- serialization/deserialization.
- - Introduce `JsonUnknwon` interface to preserve unknown properties when deserializing/serializing
- SDK classes.
- - When passing custom objects, for example in `Contexts`, these are supported for serialization:
- - `JsonSerializable`
- - `Map`, `Collection`, `Array`, `String` and all primitive types.
- - Objects with the help of refection.
- - `Map`, `Collection`, `Array`, `String` and all primitive types.
- - Call `toString()` on objects that have a cyclic reference to a ancestor object.
- - Call `toString()` where object graphs exceed max depth.
- - Remove `gson` dependency.
- - Remove `IUnknownPropertiesConsumer`
+ - Introduce `JsonSerializable` and `JsonDeserializer` interfaces for manual json
+ serialization/deserialization.
+ - Introduce `JsonUnknwon` interface to preserve unknown properties when deserializing/serializing
+ SDK classes.
+ - When passing custom objects, for example in `Contexts`, these are supported for serialization:
+ - `JsonSerializable`
+ - `Map`, `Collection`, `Array`, `String` and all primitive types.
+ - Objects with the help of refection.
+ - `Map`, `Collection`, `Array`, `String` and all primitive types.
+ - Call `toString()` on objects that have a cyclic reference to a ancestor object.
+ - Call `toString()` where object graphs exceed max depth.
+ - Remove `gson` dependency.
+ - Remove `IUnknownPropertiesConsumer`
- Pass MDC tags as Sentry tags ([#1954](https://github.com/getsentry/sentry-java/pull/1954))
### Fixes
@@ -2841,7 +3927,7 @@ Breaking Changes:
### Fixes
-* Change order of event filtering mechanisms and only send session update for dropped events if session state changed (#2028)
+- Change order of event filtering mechanisms and only send session update for dropped events if session state changed (#2028)
## 5.7.3
@@ -3235,7 +4321,6 @@ Thank you:
### Fixes
-
- Ref: Deprecate SentryBaseEvent#getOriginThrowable and add SentryBaseEvent#getThrowableMechanism ([#1502](https://github.com/getsentry/sentry-java/pull/1502))
- Graceful Shutdown flushes event instead of Closing SDK ([#1500](https://github.com/getsentry/sentry-java/pull/1500))
- Do not append threads that come from the EnvelopeFileObserver ([#1501](https://github.com/getsentry/sentry-java/pull/1501))
@@ -3394,6 +4479,7 @@ Breaking Changes:
- SentryTransaction#finish should not clear another transaction from the scope ([#1278](https://github.com/getsentry/sentry-java/pull/1278))
Breaking Changes:
+
- Enchancement: SentryExceptionResolver should not send handled errors by default ([#1248](https://github.com/getsentry/sentry-java/pull/1248)).
- Ref: Simplify RestTemplate instrumentation ([#1246](https://github.com/getsentry/sentry-java/pull/1246))
- Enchancement: Add overloads for startTransaction taking op and description ([#1244](https://github.com/getsentry/sentry-java/pull/1244))
@@ -3547,7 +4633,7 @@ This release brings the Sentry Performance feature to Java SDK, Spring, Spring B
- Set current thread only if theres no exceptions ([#1064](https://github.com/getsentry/sentry-java/pull/1064))
- Append DebugImage list if event already has it ([#1092](https://github.com/getsentry/sentry-java/pull/1092))
- Sort breadcrumbs by Date if there are breadcrumbs already in the event ([#1094](https://github.com/getsentry/sentry-java/pull/1094))
-- Free Local Refs manually due to Android local ref. count limits ([#1179](https://github.com/getsentry/sentry-java/pull/1179))
+- Free Local Refs manually due to Android local ref. count limits ([#1179](https://github.com/getsentry/sentry-java/pull/1179))
## 3.2.0
@@ -3668,6 +4754,7 @@ Packages were released on [`bintray sentry-java`](https://dl.bintray.com/getsent
## Where is the Java 1.7 code base?
The previous Java releases, are all available in this repository through the tagged releases.
+
## 3.0.0-beta.1
## What’s Changed
@@ -3707,7 +4794,7 @@ TBD
Packages were released on [bintray](https://dl.bintray.com/getsentry/maven/io/sentry/)
> Note: This release marks the unification of the Java and Android Sentry codebases based on the core of the Android SDK (version 2.x).
-Previous releases for the Android SDK (version 2.x) can be found on the now archived: https://github.com/getsentry/sentry-android/
+> Previous releases for the Android SDK (version 2.x) can be found on the now archived: https://github.com/getsentry/sentry-android/
## 3.0.0-alpha.1
@@ -3715,7 +4802,6 @@ Previous releases for the Android SDK (version 2.x) can be found on the now arch
### Fixes
-
## New releases will happen on a different repository:
https://github.com/getsentry/sentry-java
@@ -3726,7 +4812,6 @@ https://github.com/getsentry/sentry-java
### Fixes
-
- feat: enable release health by default
Packages were released on [`bintray`](https://dl.bintray.com/getsentry/sentry-android/io/sentry/sentry-android/), [`jcenter`](https://jcenter.bintray.com/io/sentry/sentry-android/) and [`mavenCentral`](https://repo.maven.apache.org/maven2/io/sentry/sentry-android/)
@@ -3810,15 +4895,15 @@ We'd love to get feedback.
- feat: timber integration ([#464](https://github.com/getsentry/sentry-android/pull/464)) @marandaneto
-1) To add integrations it requires a [manual initialization](https://docs.sentry.io/platforms/android/#manual-initialization) of the Android SDK.
+1. To add integrations it requires a [manual initialization](https://docs.sentry.io/platforms/android/#manual-initialization) of the Android SDK.
-2) Add the `sentry-android-timber` dependency:
+2. Add the `sentry-android-timber` dependency:
```groovy
implementation 'io.sentry:sentry-android-timber:{version}' // version >= 2.2.0
```
-3) Initialize and add the `SentryTimberIntegration`:
+3. Initialize and add the `SentryTimberIntegration`:
```java
SentryAndroid.init(this, options -> {
@@ -3832,7 +4917,7 @@ SentryAndroid.init(this, options -> {
});
```
-4) Use the Timber integration:
+4. Use the Timber integration:
```java
try {
@@ -4121,8 +5206,8 @@ New features not offered by (1.7.x):
- Captures crashes caused by native code
- Access to the [`sentry-native` SDK](https://github.com/getsentry/sentry-native/) API by your native (C/C++/Rust code/..).
- Automatic init (just add your `DSN` to the manifest)
- - Proguard rules are added automatically
- - Permission (Internet) is added automatically
+ - Proguard rules are added automatically
+ - Permission (Internet) is added automatically
- Uncaught Exceptions might be captured even before the app restarts
- Sentry's Unified API.
- More context/device information
@@ -4183,7 +5268,6 @@ Release of Sentry's new SDK for Android.
### Fixes
-
- Update ndk for new sentry-native version ([#235](https://github.com/getsentry/sentry-android/pull/235)) @Swatinem @marandaneto
- Make integrations public ([#256](https://github.com/getsentry/sentry-android/pull/256)) @marandaneto
- Bump build-tools ([#255](https://github.com/getsentry/sentry-android/pull/255)) @marandaneto
@@ -4221,7 +5305,6 @@ Release of Sentry's new SDK for Android.
### Fixes
-
- Honor RetryAfter ([#236](https://github.com/getsentry/sentry-android/pull/236)) @marandaneto
- Add tests for SentryValues ([#238](https://github.com/getsentry/sentry-android/pull/238)) @philipphofmann
- Do not set frames if there's none ([#234](https://github.com/getsentry/sentry-android/pull/234)) @marandaneto
@@ -4322,7 +5405,7 @@ Third release of Sentry's new SDK for Android.
### Fixes
-- Fixed release for jcenter and bintray
+- Fixed release for jcenter and bintray
Packages were released on [`bintray`](https://dl.bintray.com/getsentry/sentry-android/io/sentry/), [`jcenter`](https://jcenter.bintray.com/io/sentry/sentry-android/)
@@ -4352,8 +5435,8 @@ New features not offered by our current (1.7.x), stable SDK are:
- Captures crashes caused by native code
- Access to the [`sentry-native` SDK](https://github.com/getsentry/sentry-native/) API by your native (C/C++/Rust code/..).
- Automatic init (just add your `DSN` to the manifest)
- - Proguard rules are added automatically
- - Permission (Internet) is added automatically
+ - Proguard rules are added automatically
+ - Permission (Internet) is added automatically
- Uncaught Exceptions might be captured even before the app restarts
- Unified API which include scopes etc.
- More context/device information
diff --git a/CLAUDE.md b/CLAUDE.md
index 9de4130c1a7..f59e5a152f3 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,156 +1,9 @@
# CLAUDE.md
-This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+## STOP — Required Reading (Do This First)
-## Project Overview
-
-This is the Sentry Java/Android SDK - a comprehensive error monitoring and performance tracking SDK for Java and Android applications. The repository contains multiple modules for different integrations and platforms.
-
-## Build System
-
-The project uses **Gradle** with Kotlin DSL. Key build files:
-- `build.gradle.kts` - Root build configuration
-- `settings.gradle.kts` - Multi-module project structure
-- `buildSrc/` and `build-logic/` - Custom build logic and plugins
-- `Makefile` - High-level build commands
-
-## Essential Commands
-
-### Development Workflow
-```bash
-# Format code and regenerate .api files (REQUIRED before committing)
-./gradlew spotlessApply apiDump
-
-# Run all tests and linter
-./gradlew check
-
-# Build entire project
-./gradlew build
-
-# Create coverage reports
-./gradlew jacocoTestReport koverXmlReportRelease
-
-# Generate documentation
-./gradlew aggregateJavadocs
-```
-
-### Testing
-```bash
-# Run unit tests for a specific file
-./gradlew '::testDebugUnitTest' --tests="**" --info
-
-# Run system tests (requires Python virtual env)
-make systemTest
-
-# Run specific test suites
-./gradlew :sentry-android-core:testDebugUnitTest
-./gradlew :sentry:test
-```
-
-### Code Quality
-```bash
-# Check code formatting
-./gradlew spotlessJavaCheck spotlessKotlinCheck
-
-# Apply code formatting
-./gradlew spotlessApply
-
-# Update API dump files (after API changes)
-./gradlew apiDump
-
-# Dependency updates check
-./gradlew dependencyUpdates -Drevision=release
-```
-
-### Android-Specific Commands
-```bash
-# Assemble Android test APKs
-./gradlew :sentry-android-integration-tests:sentry-uitest-android:assembleRelease
-./gradlew :sentry-android-integration-tests:sentry-uitest-android:assembleAndroidTest -DtestBuildType=release
-
-# Run critical UI tests
-./scripts/test-ui-critical.sh
-```
-
-## Development Workflow Rules
-
-### Planning and Implementation Process
-1. **First think through the problem**: Read the codebase for relevant files and propose a plan
-2. **Check in before beginning**: Verify the plan before starting implementation
-3. **Use todo tracking**: Work through todo items, marking them as complete as you go
-4. **High-level communication**: Give high-level explanations of changes made, not step-by-step descriptions
-5. **Simplicity first**: Make every task and code change as simple as possible. Avoid massive or complex changes. Impact as little code as possible.
-6. **Format and regenerate**: Once done, format code and regenerate .api files: `./gradlew spotlessApply apiDump`
-7. **Propose commit**: As final step, git stage relevant files and propose (but not execute) a single git commit command
-
-## Module Architecture
-
-The repository is organized into multiple modules:
-
-### Core Modules
-- **`sentry`** - Core Java SDK implementation
-- **`sentry-android-core`** - Core Android SDK implementation
-- **`sentry-android`** - High-level Android SDK
-
-### Integration Modules
-- **Spring Framework**: `sentry-spring*`, `sentry-spring-boot*`
-- **Logging**: `sentry-logback`, `sentry-log4j2`, `sentry-jul`
-- **Web**: `sentry-servlet*`, `sentry-okhttp`, `sentry-apache-http-client-5`
-- **GraphQL**: `sentry-graphql*`, `sentry-apollo*`
-- **Android UI**: `sentry-android-fragment`, `sentry-android-navigation`, `sentry-compose`
-- **Reactive**: `sentry-reactor`, `sentry-ktor-client`
-- **Monitoring**: `sentry-opentelemetry*`, `sentry-quartz`
-
-### Utility Modules
-- **`sentry-test-support`** - Shared test utilities
-- **`sentry-system-test-support`** - System testing infrastructure
-- **`sentry-samples`** - Example applications
-- **`sentry-bom`** - Bill of Materials for dependency management
-
-### Key Architectural Patterns
-- **Multi-platform**: Supports JVM, Android, and Kotlin Multiplatform (Compose modules)
-- **Modular Design**: Each integration is a separate module with minimal dependencies
-- **Options Pattern**: Features are opt-in via `SentryOptions` and similar configuration classes
-- **Transport Layer**: Pluggable transport implementations for different environments
-- **Scope Management**: Thread-safe scope/context management for error tracking
-
-## Development Guidelines
-
-### Code Style
-- **Languages**: Java 8+ and Kotlin
-- **Formatting**: Enforced via Spotless - always run `./gradlew spotlessApply` before committing
-- **API Compatibility**: Binary compatibility is enforced - run `./gradlew apiDump` after API changes
-
-### Testing Requirements
-- Write comprehensive unit tests for new features
-- Android modules require both unit tests and instrumented tests where applicable
-- System tests validate end-to-end functionality with sample applications
-- Coverage reports are generated for both JaCoCo (Java/Android) and Kover (KMP modules)
-
-### Contributing Guidelines
-1. Follow existing code style and language
-2. Do not modify API files (e.g. sentry.api) manually - run `./gradlew apiDump` to regenerate them
-3. Write comprehensive tests
-4. New features must be **opt-in by default** - extend `SentryOptions` or similar Option classes with getters/setters
-5. Consider backwards compatibility
-
-## Domain-Specific Knowledge Areas
-
-For complex SDK functionality, refer to the detailed cursor rules in `.cursor/rules/`:
-
-- **Scopes and Hub Management**: See `.cursor/rules/scopes.mdc` for details on `IScopes`, scope types (global/isolation/current), thread-local storage, forking behavior, and v7→v8 migration patterns
-- **Event Deduplication**: See `.cursor/rules/deduplication.mdc` for `DuplicateEventDetectionEventProcessor` and `enableDeduplication` option
-- **Offline Behavior and Caching**: See `.cursor/rules/offline.mdc` for envelope caching, retry logic, transport behavior, and Android vs JVM differences
-- **OpenTelemetry Integration**: See `.cursor/rules/opentelemetry.mdc` for agent vs agentless modes, span processing, context propagation, and configuration
-- **System Testing (E2E)**: See `.cursor/rules/e2e_tests.mdc` for system test framework, mock server setup, and CI workflows
-
-### Usage Pattern
-When working on these specific areas, read the corresponding cursor rule file first to understand the detailed architecture, then proceed with implementation.
-
-## Useful Resources
-
-- Main SDK documentation: https://develop.sentry.dev/sdk/overview/
-- Internal contributing guide: https://docs.sentry.io/internal/contributing/
-- Git commit message conventions: https://develop.sentry.dev/engineering-practices/commit-messages/
-
-This SDK is production-ready and used by thousands of applications. Changes should be thoroughly tested and maintain backwards compatibility.
\ No newline at end of file
+Before doing ANYTHING else (including answering questions), you MUST use the Read tool to load
+[AGENTS.md](AGENTS.md) and follow ALL of its instructions. It is the single source of truth
+for build commands, contributing guidelines, workflow rules, and the index of the
+domain-specific rules.
+Do NOT skip this step. Do NOT proceed without reading it first.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8e2c8b78bf1..f4354c72a89 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -57,7 +57,19 @@ or
However, if your change did not intend to modify the public API, consider changing the method/property visibility or removing the change altogether.
+# Linking issues
+
+If a PR should notify a linked issue after release, use a GitHub closing keyword in the PR
+description, such as `Fixes #123`, `Closes #123`, or `Resolves #123`. Release notification
+automation only comments on issues GitHub recognizes as closed by the released PR; mentioning an
+issue without a closing keyword is not enough.
+
# CI
Build and tests are automatically run against branches and pull requests
via GH Actions.
+
+
+# AI Use
+
+You are welcome to use whatever tools you prefer for making a contribution. However, any changes you propose have to be reviewed and tested by you, a human, first, before you submit a pull request with them for the Sentry team to review. If we feel like that did not happen, we will close the PR outright. For example, we will not review visibly AI-generated PRs from an agent instructed to look for and "fix" open issues in the repo. This aligns with our SDK principle: [every line has an owner](https://develop.sentry.dev/sdk/getting-started/principles/#every-line-has-an-owner).
diff --git a/Makefile b/Makefile
index 55f465a9663..3967ff856ad 100644
--- a/Makefile
+++ b/Makefile
@@ -1,9 +1,9 @@
-.PHONY: all clean compile javadocs dryRelease update checkFormat api assembleBenchmarkTestRelease assembleUiTestRelease assembleUiTestCriticalRelease createCoverageReports runUiTestCritical setupPython systemTest systemTestInteractive check preMerge publish
+.PHONY: all clean compile javadocs dryRelease update checkFormat api assembleBenchmarkTestRelease assembleUiTestRelease assembleUiTestCriticalRelease runUiTestCritical setupPython systemTest systemTestInteractive check preMerge publish
-all: stop clean javadocs compile createCoverageReports
+all: stop clean javadocs compile
assembleBenchmarks: assembleBenchmarkTestRelease
assembleUiTests: assembleUiTestRelease
-preMerge: check createCoverageReports
+preMerge: check
publish: clean dryRelease
# deep clean
@@ -37,13 +37,11 @@ api:
# Assemble release and Android test apk of the uitest-android-benchmark module
assembleBenchmarkTestRelease:
- ./gradlew :sentry-android-integration-tests:sentry-uitest-android-benchmark:assembleRelease
- ./gradlew :sentry-android-integration-tests:sentry-uitest-android-benchmark:assembleAndroidTest -DtestBuildType=release
+ ./gradlew :sentry-android-integration-tests:sentry-uitest-android-benchmark:assembleRelease :sentry-android-integration-tests:sentry-uitest-android-benchmark:assembleAndroidTest
# Assemble release and Android test apk of the uitest-android module
assembleUiTestRelease:
- ./gradlew :sentry-android-integration-tests:sentry-uitest-android:assembleRelease
- ./gradlew :sentry-android-integration-tests:sentry-uitest-android:assembleAndroidTest -DtestBuildType=release
+ ./gradlew :sentry-android-integration-tests:sentry-uitest-android:assembleRelease :sentry-android-integration-tests:sentry-uitest-android:assembleAndroidTest
# Assemble release of the uitest-android-critical module
assembleUiTestCriticalRelease:
@@ -53,13 +51,6 @@ assembleUiTestCriticalRelease:
runUiTestCritical:
./scripts/test-ui-critical.sh
-# Create coverage reports
-# - Jacoco for Java & Android modules
-# - Kover for KMP modules e.g sentry-compose
-createCoverageReports:
- ./gradlew jacocoTestReport
- ./gradlew koverXmlReportRelease
-
# Create the Python virtual environment for system tests, and install the necessary dependencies
setupPython:
@test -d .venv || python3 -m venv .venv
diff --git a/README.md b/README.md
index 096ecf7d19a..849aaf74457 100644
--- a/README.md
+++ b/README.md
@@ -13,55 +13,65 @@ _Bad software is everywhere, and we're tired of it. Sentry is on a mission to he
Sentry SDK for Java and Android
===========
[](https://github.com/getsentry/sentry-java/actions)
-[](https://codecov.io/gh/getsentry/sentry-java)
+[](https://x.com/intent/follow?screen_name=sentry)
[](https://discord.gg/PXa5Apfe7K)
-| Packages | Maven Central | Minimum Android API Version |
-|-----------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| ------- |
-| sentry-android | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android) | 21 |
-| sentry-android-core | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-core) | 21 |
-| sentry-android-ndk | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-ndk) | 21 |
-| sentry-android-timber | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-timber) | 21 |
-| sentry-android-fragment | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-fragment) | 21 |
-| sentry-android-navigation | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-navigation) | 21 |
-| sentry-android-sqlite | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-sqlite) | 21 |
-| sentry-android-replay | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-android-replay) | 26 |
-| sentry-compose-android | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-compose-android) | 21 |
-| sentry-compose-desktop | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-compose-desktop) |
-| sentry-compose | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-compose) |
-| sentry-apache-http-client-5 | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apache-http-client-5) |
-| sentry | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry) | 21 |
-| sentry-jul | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-jul) |
-| sentry-jdbc | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-jdbc) |
-| sentry-apollo | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apollo) | 21 |
-| sentry-apollo-3 | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apollo-3) | 21 |
-| sentry-apollo-4 | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-apollo-4) | 21 |
-| sentry-kotlin-extensions | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-kotlin-extensions) | 21 |
-| sentry-ktor-client | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-ktor-client) | 21 |
-| sentry-servlet | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-servlet) | |
-| sentry-servlet-jakarta | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-servlet-jakarta) | |
-| sentry-spring-boot | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-boot) |
-| sentry-spring-boot-jakarta | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-boot-jakarta) |
-| sentry-spring-boot-4 | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-boot-4) |
-| sentry-spring-boot-4-starter | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-boot-4-starter) |
-| sentry-spring-boot-starter | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-boot-starter) |
-| sentry-spring-boot-starter-jakarta | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-boot-starter-jakarta) |
-| sentry-spring | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring) |
-| sentry-spring-jakarta | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-jakarta) |
-| sentry-spring-7 | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-spring-7) |
-| sentry-logback | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-logback) |
-| sentry-log4j2 | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-log4j2) |
-| sentry-bom | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-bom) |
-| sentry-graphql | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql) |
-| sentry-graphql-core | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql-core) |
-| sentry-graphql-22 | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql-22) |
-| sentry-quartz | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-quartz) |
-| sentry-openfeign | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-openfeign) |
-| sentry-opentelemetry-agent | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-opentelemetry-agent) |
-| sentry-opentelemetry-agentcustomization | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-opentelemetry-agentcustomization) |
-| sentry-opentelemetry-core | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-opentelemetry-core) |
-| sentry-okhttp | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-okhttp) |
-| sentry-reactor | [](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-reactor) |
+| Packages | Maven Central | Minimum Android API Version |
+|-----------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------| ------- |
+| sentry-android | [](https://central.sonatype.com/artifact/io.sentry/sentry-android) | 21 |
+| sentry-android-core | [](https://central.sonatype.com/artifact/io.sentry/sentry-android-core) | 21 |
+| sentry-android-distribution | [](https://central.sonatype.com/artifact/io.sentry/sentry-android-distribution) | 21 |
+| sentry-android-ndk | [](https://central.sonatype.com/artifact/io.sentry/sentry-android-ndk) | 21 |
+| sentry-android-timber | [](https://central.sonatype.com/artifact/io.sentry/sentry-android-timber) | 21 |
+| sentry-android-fragment | [](https://central.sonatype.com/artifact/io.sentry/sentry-android-fragment) | 21 |
+| sentry-android-navigation | [](https://central.sonatype.com/artifact/io.sentry/sentry-android-navigation) | 21 |
+| sentry-android-sqlite | [](https://central.sonatype.com/artifact/io.sentry/sentry-android-sqlite) | 21 |
+| sentry-android-replay | [](https://central.sonatype.com/artifact/io.sentry/sentry-android-replay) | 26 |
+| sentry-compose-android | [](https://central.sonatype.com/artifact/io.sentry/sentry-compose-android) | 21 |
+| sentry-compose-desktop | [](https://central.sonatype.com/artifact/io.sentry/sentry-compose-desktop) |
+| sentry-compose | [](https://central.sonatype.com/artifact/io.sentry/sentry-compose) |
+| sentry-apache-http-client-5 | [](https://central.sonatype.com/artifact/io.sentry/sentry-apache-http-client-5) |
+| sentry | [](https://central.sonatype.com/artifact/io.sentry/sentry) | 21 |
+| sentry-jul | [](https://central.sonatype.com/artifact/io.sentry/sentry-jul) |
+| sentry-jdbc | [](https://central.sonatype.com/artifact/io.sentry/sentry-jdbc) |
+| sentry-kafka | [](https://central.sonatype.com/artifact/io.sentry/sentry-kafka) |
+| sentry-apollo | [](https://central.sonatype.com/artifact/io.sentry/sentry-apollo) | 21 |
+| sentry-apollo-3 | [](https://central.sonatype.com/artifact/io.sentry/sentry-apollo-3) | 21 |
+| sentry-apollo-4 | [](https://central.sonatype.com/artifact/io.sentry/sentry-apollo-4) | 21 |
+| sentry-kotlin-extensions | [](https://central.sonatype.com/artifact/io.sentry/sentry-kotlin-extensions) | 21 |
+| sentry-ktor-client | [](https://central.sonatype.com/artifact/io.sentry/sentry-ktor-client) | 21 |
+| sentry-servlet | [](https://central.sonatype.com/artifact/io.sentry/sentry-servlet) | |
+| sentry-servlet-jakarta | [](https://central.sonatype.com/artifact/io.sentry/sentry-servlet-jakarta) | |
+| sentry-spring-boot | [](https://central.sonatype.com/artifact/io.sentry/sentry-spring-boot) |
+| sentry-spring-boot-jakarta | [](https://central.sonatype.com/artifact/io.sentry/sentry-spring-boot-jakarta) |
+| sentry-spring-boot-4 | [](https://central.sonatype.com/artifact/io.sentry/sentry-spring-boot-4) |
+| sentry-spring-boot-4-starter | [](https://central.sonatype.com/artifact/io.sentry/sentry-spring-boot-4-starter) |
+| sentry-spring-boot-starter | [](https://central.sonatype.com/artifact/io.sentry/sentry-spring-boot-starter) |
+| sentry-spring-boot-starter-jakarta | [](https://central.sonatype.com/artifact/io.sentry/sentry-spring-boot-starter-jakarta) |
+| sentry-spring | [](https://central.sonatype.com/artifact/io.sentry/sentry-spring) |
+| sentry-spring-jakarta | [](https://central.sonatype.com/artifact/io.sentry/sentry-spring-jakarta) |
+| sentry-spring-7 | [](https://central.sonatype.com/artifact/io.sentry/sentry-spring-7) |
+| sentry-logback | [](https://central.sonatype.com/artifact/io.sentry/sentry-logback) |
+| sentry-log4j2 | [](https://central.sonatype.com/artifact/io.sentry/sentry-log4j2) |
+| sentry-bom | [](https://central.sonatype.com/artifact/io.sentry/sentry-bom) |
+| sentry-graphql | [](https://central.sonatype.com/artifact/io.sentry/sentry-graphql) |
+| sentry-graphql-core | [](https://central.sonatype.com/artifact/io.sentry/sentry-graphql-core) |
+| sentry-graphql-22 | [](https://central.sonatype.com/artifact/io.sentry/sentry-graphql-22) |
+| sentry-jcache | [](https://central.sonatype.com/artifact/io.sentry/sentry-jcache) |
+| sentry-quartz | [](https://central.sonatype.com/artifact/io.sentry/sentry-quartz) |
+| sentry-openfeign | [](https://central.sonatype.com/artifact/io.sentry/sentry-openfeign) |
+| sentry-openfeature | [](https://central.sonatype.com/artifact/io.sentry/sentry-openfeature) |
+| sentry-launchdarkly-android | [](https://central.sonatype.com/artifact/io.sentry/sentry-launchdarkly-android) |
+| sentry-launchdarkly-server | [](https://central.sonatype.com/artifact/io.sentry/sentry-launchdarkly-server) |
+| sentry-opentelemetry-agent | [](https://central.sonatype.com/artifact/io.sentry/sentry-opentelemetry-agent) |
+| sentry-opentelemetry-bom | [](https://central.sonatype.com/artifact/io.sentry/sentry-opentelemetry-bom) |
+| sentry-opentelemetry-agentcustomization | [](https://central.sonatype.com/artifact/io.sentry/sentry-opentelemetry-agentcustomization) |
+| sentry-opentelemetry-core | [](https://central.sonatype.com/artifact/io.sentry/sentry-opentelemetry-core) |
+| sentry-opentelemetry-otlp | [](https://central.sonatype.com/artifact/io.sentry/sentry-opentelemetry-otlp) |
+| sentry-opentelemetry-otlp-spring | [](https://central.sonatype.com/artifact/io.sentry/sentry-opentelemetry-otlp-spring) |
+| sentry-okhttp | [](https://central.sonatype.com/artifact/io.sentry/sentry-okhttp) |
+| sentry-reactor | [](https://central.sonatype.com/artifact/io.sentry/sentry-reactor) |
+| sentry-spotlight | [](https://central.sonatype.com/artifact/io.sentry/sentry-spotlight) |
# Releases
diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md
new file mode 100644
index 00000000000..a0040916145
--- /dev/null
+++ b/THIRD_PARTY_NOTICES.md
@@ -0,0 +1,598 @@
+# Third-Party Software Notices and Information
+
+The Sentry Java SDK distribution includes software developed by third parties which carry their own copyright notices and license terms. These notices are provided below.
+
+In the event that a required notice is missing or incorrect, please inform us by creating an issue [here](https://github.com/getsentry/sentry-java/issues).
+
+---
+
+## Google GSON (Apache 2.0)
+
+**Source:** https://github.com/google/gson (Tag: gson-parent-2.8.7)
+**License:** Apache License 2.0
+**Copyright:** Copyright (C) 2010 Google Inc.
+
+### Scope
+
+The Sentry Java SDK includes vendored JSON stream reading and writing classes extracted from the GSON library. The code resides in the `io.sentry.vendor.gson.stream` package and includes `JsonReader`, `JsonWriter`, `JsonScope`, `JsonToken`, and `MalformedJsonException`.
+
+```
+Copyright (C) 2010 Google Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+```
+
+---
+
+## Google Guava — LongMath (Apache 2.0)
+
+**Source:** https://github.com/google/guava/blob/v33.0.0/guava/src/com/google/common/math/LongMath.java
+**License:** Apache License 2.0
+**Copyright:** Copyright (C) 2011 The Guava Authors
+
+### Scope
+
+The Sentry Java SDK includes adapted floor division logic from Guava's `LongMath` class to support older Android API levels. The code resides in `io.sentry.vendor.SentryMath`.
+
+```
+Copyright (C) 2011 The Guava Authors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+```
+
+---
+
+## FasterXML Jackson — ISO8601Utils (Apache 2.0)
+
+**Source:** https://github.com/FasterXML/jackson-databind
+**License:** Apache License 2.0
+**Copyright:** Copyright (C) 2007-, Tatu Saloranta
+
+### Scope
+
+The Sentry Java SDK includes an adapted version of `ISO8601Utils` from the Jackson Databind library for ISO 8601 date/time parsing and formatting. The code resides in `io.sentry.vendor.gson.internal.bind.util.ISO8601Utils`.
+
+```
+Copyright (C) 2007-, Tatu Saloranta
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+```
+
+---
+
+## Howard Hinnant — Date Algorithms (Public Domain)
+
+**Source:** https://howardhinnant.github.io/date_algorithms.html
+**License:** Public Domain
+**Copyright:** None; public domain dedication by Howard Hinnant
+
+### Scope
+
+The Sentry Java SDK includes adapted civil date conversion algorithms from Howard Hinnant's date algorithms for UTC ISO 8601 timestamp parsing and formatting. The code resides in `io.sentry.vendor.SentryIso8601Utils`.
+
+```
+Consider these donated to the public domain.
+```
+
+---
+
+## Android Open Source Project — Base64 (Apache 2.0)
+
+**Source:** https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/util/Base64.java
+**License:** Apache License 2.0
+**Copyright:** Copyright (C) 2010 The Android Open Source Project
+
+### Scope
+
+The Sentry Java SDK includes an adapted version of the Android `Base64` class for Base64 encoding and decoding on non-Android platforms. The code resides in `io.sentry.vendor.Base64`.
+
+```
+Copyright (C) 2010 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+```
+
+---
+
+## Square — Tape (Apache 2.0)
+
+**Source:** https://github.com/square/tape (Commit: 445cd3fd0a7b3ec48c9ea3e0e86663fe6d3735d8, archived 2024-10-25)
+**License:** Apache License 2.0
+**Copyright:** Copyright (C) 2010 Square, Inc.
+
+### Scope
+
+The Sentry Java SDK includes an adapted version of Square's Tape library, a file-based FIFO queue implementation used for reliable event storage. The code resides in the `io.sentry.cache.tape` package and includes `QueueFile`, `FileObjectQueue`, and `ObjectQueue`.
+
+Upstream was archived on 2024-10-25 and is no longer maintained. This copy is maintained in-tree and has diverged from the linked commit: it recovers from file corruption by recreating the file, bounds the queue to a maximum number of elements, and supports optional buffered writes flushed by an explicit `sync()`.
+
+```
+Copyright (C) 2010 Square, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+```
+
+---
+
+## Square — Seismic (Apache 2.0)
+
+**Source:** https://github.com/square/seismic
+**License:** Apache License 2.0
+**Copyright:** Copyright 2010 Square, Inc.
+
+### Scope
+
+The Sentry Java SDK includes an adapted version of Square's Seismic shake detection algorithm. The rolling sample window approach and `SampleQueue`/`SamplePool` data structures in `io.sentry.android.core.SentryShakeDetector` are based on Seismic's `ShakeDetector`.
+
+```
+Copyright 2010 Square, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+```
+
+---
+
+## Square — Curtains (Apache 2.0)
+
+**Source:** https://github.com/square/curtains (v1.2.5)
+**License:** Apache License 2.0
+**Copyright:** Copyright 2021 Square Inc.
+
+### Scope
+
+The Sentry Java SDK includes adapted versions of Square's Curtains library for null-safe `Window.Callback` handling and for tracking attached window roots. The code resides in `io.sentry.android.replay.util.FixedWindowCallback` and `io.sentry.android.replay.Windows`.
+
+```
+Copyright 2021 Square Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+```
+
+---
+
+## Apache Commons Collections (Apache 2.0)
+
+**Source:** https://github.com/apache/commons-collections
+**License:** Apache License 2.0
+**Copyright:** Copyright The Apache Software Foundation
+
+### Scope
+
+The Sentry Java SDK includes adapted versions of `CircularFifoQueue`, `SynchronizedCollection`, and `SynchronizedQueue` from Apache Commons Collections. The code resides in `io.sentry.CircularFifoQueue`, `io.sentry.SynchronizedCollection`, and `io.sentry.SynchronizedQueue`.
+
+```
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+```
+
+---
+
+## Matej Tymes — JavaFixes (Apache 2.0)
+
+**Source:** https://github.com/MatejTymes/JavaFixes (Commit: 37e74b9d0a29f7a47485c6d1bb1307f01fb93634)
+**License:** Apache License 2.0
+**Copyright:** Copyright (C) 2016 Matej Tymes
+
+### Scope
+
+The Sentry Java SDK includes an adapted version of `ReusableCountLatch` from the JavaFixes library for concurrent synchronization. The code resides in `io.sentry.transport.ReusableCountLatch`.
+
+```
+Copyright (C) 2016 Matej Tymes
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+```
+
+---
+
+## Baomidou — Dynamic-Datasource (Apache 2.0)
+
+**Source:** https://github.com/baomidou/dynamic-datasource
+**License:** Apache License 2.0
+**Copyright:** Copyright © 2018 organization baomidou
+
+### Scope
+
+The Sentry Java SDK includes an adapted UUID generation implementation from the Dynamic-Datasource library. The code resides in `io.sentry.util.UUIDGenerator`.
+
+```
+Copyright © 2018 organization baomidou
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+```
+
+---
+
+## Google Firebase — Android SDK (Apache 2.0)
+
+**Source:** https://github.com/firebase/firebase-android-sdk
+**License:** Apache License 2.0
+**Copyright:** Copyright 2022 Google LLC
+
+### Scope
+
+The Sentry Java SDK includes an adapted version of `FirstDrawDoneListener` from the Firebase Android SDK for detecting initial display time via `OnDrawListener`. The code resides in `io.sentry.android.core.internal.util.FirstDrawDoneListener`.
+
+```
+Copyright 2022 Google LLC
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+```
+
+---
+
+## Android Open Source Project — Thread Dump Parsing (Apache 2.0)
+
+**Source:** https://cs.android.com/android/platform/superproject/+/master:development/tools/bugreport/src/com/android/bugreport/stacks/ThreadSnapshotParser.java
+**License:** Apache License 2.0
+**Copyright:** Copyright (C) 2016 The Android Open Source Project
+
+### Scope
+
+The Sentry Java SDK includes adapted thread state and stack trace parsing code from the Android Open Source Project's bugreport tools. The code resides in the `io.sentry.android.core.internal.threaddump` package and includes `ThreadDumpParser`, `Line`, and `Lines`.
+
+```
+Copyright (C) 2016 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+```
+
+---
+
+## Android Open Source Project — Jetpack Compose UI (Apache 2.0)
+
+**Source:** https://github.com/androidx/androidx/blob/fc7df0dd68466ac3bb16b1c79b7a73dd0bfdd4c1/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt#L187
+**Source:** https://github.com/androidx/androidx/blob/androidx-main/compose/ui/ui-util/src/commonMain/kotlin/androidx/compose/ui/util/MathHelpers.kt
+**License:** Apache License 2.0
+**Copyright:** Copyright (C) 2019, 2020 The Android Open Source Project
+
+### Scope
+
+The Sentry Android Replay SDK includes code adapted from Jetpack Compose UI, used to compute Compose node bounds while traversing the view hierarchy for masking. The code resides in `io.sentry.android.replay.util.Nodes`: the `boundsInWindow` extension function (a faster copy of `LayoutCoordinates.boundsInWindow`) and the `fastMinOf`, `fastMaxOf`, `fastCoerceIn`, `fastCoerceAtLeast`, and `fastCoerceAtMost` numeric helpers (copied from `androidx.compose.ui.util.MathHelpers`).
+
+```
+Copyright (C) 2019, 2020 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+```
+
+---
+
+## OpenTelemetry (Apache 2.0)
+
+**Source:** https://github.com/open-telemetry/opentelemetry-java (Commit: 0aacc55d1e3f5cc6dbb4f8fa26bcb657b01a7bc9)
+**License:** Apache License 2.0
+**Copyright:** Copyright The OpenTelemetry Authors
+
+### Scope
+
+The Sentry Java SDK includes an adapted version of `ThreadLocalContextStorage` from the OpenTelemetry Java SDK for thread-local context storage. The code resides in `io.sentry.opentelemetry.SentryOtelThreadLocalStorage`.
+
+```
+Copyright The OpenTelemetry Authors
+SPDX-License-Identifier: Apache-2.0
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+```
+
+---
+
+## SalomonBrys — ANR-WatchDog (MIT)
+
+**Source:** https://github.com/SalomonBrys/ANR-WatchDog (Commit: 1969075f75f5980e9000eaffbaa13b0daf282dcb)
+**License:** MIT License
+**Copyright:** Copyright (c) 2016 Salomon BRYS
+
+### Scope
+
+The Sentry Java SDK includes an adapted version of the ANR-WatchDog library for Application Not Responding (ANR) detection on Android. The code resides in `io.sentry.android.core.ANRWatchDog`.
+
+```
+MIT License
+
+Copyright (c) 2016 Salomon BRYS
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+```
+
+---
+
+## Breadwallet — Root Detection (MIT)
+
+**Source:** https://github.com/Menwitz/ravencoin-android (adapted from breadwallet)
+**License:** MIT License
+**Copyright:** Copyright (c) 2016 breadwallet LLC
+
+### Scope
+
+The Sentry Java SDK includes an adapted root detection implementation from the Ravencoin Android wallet (originally from breadwallet). The code resides in `io.sentry.android.core.internal.util.RootChecker`.
+
+```
+MIT License
+
+Copyright (c) 2016 breadwallet LLC
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+```
+
+---
+
+## KilianB — PCG-Java (MIT)
+
+**Source:** https://github.com/KilianB/pcg-java
+**License:** MIT License
+**Copyright:** Copyright (c) 2018
+
+### Scope
+
+The Sentry Java SDK includes an adapted PCG-based random number generator from the pcg-java library for fast sampling. The code resides in `io.sentry.util.Random`.
+
+```
+MIT License
+
+Copyright (c) 2018
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+---
+
+## Jon Chambers — UUID String Utils (MIT)
+
+**Source:** Jon Chambers
+**License:** MIT License
+**Copyright:** Copyright (c) 2018 Jon Chambers
+
+### Scope
+
+The Sentry Java SDK includes adapted UUID string manipulation utilities. The code resides in `io.sentry.util.UUIDStringUtils`.
+
+```
+MIT License
+
+Copyright (c) 2018 Jon Chambers
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
+---
+
+## fzyzcjy — Flutter Screen Recorder (MIT)
+
+**Source:** https://github.com/fzyzcjy/flutter_screen_recorder (Commit: dce41cec25c66baf42c6bac4198e95874ce3eb9d)
+**License:** MIT License
+**Copyright:** Copyright (c) 2021 fzyzcjy
+
+### Scope
+
+The Sentry Android Replay SDK includes adapted versions of the video encoding and muxing classes from the flutter_screen_recorder library, used to encode and mux replay video frames into an MP4 file. The code resides in the `io.sentry.android.replay.video` package and includes `SimpleFrameMuxer`, `SimpleMp4FrameMuxer`, and `SimpleVideoEncoder`.
+
+```
+Copyright (c) 2021 fzyzcjy
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
+associated documentation files (the "Software"), to deal in the Software without restriction,
+including without limitation the rights to use, copy, modify, merge, publish, distribute,
+sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or
+substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
+NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+In addition to the standard MIT license, this library requires the following: The recorder itself
+only saves data on user's phone locally, thus it does not have any privacy problem. However, if
+you are going to get the records out of the local storage (e.g. upload the records to your
+server), please explicitly ask the user for permission, and promise to only use the records to
+debug your app. This is a part of the license of this library.
+```
diff --git a/agents.toml b/agents.toml
new file mode 100644
index 00000000000..d9770ee7df5
--- /dev/null
+++ b/agents.toml
@@ -0,0 +1,41 @@
+# Whenever you make changes to this file, run the following to update all generated dotagent files
+# npx @sentry/dotagents install
+# npx @sentry/dotagents sync
+
+version = 1
+
+[[skills]]
+name = "dotagents"
+source = "getsentry/dotagents"
+
+[[skills]]
+name = "sentry-workflow"
+source = "getsentry/sentry-for-ai"
+
+[[skills]]
+name = "sentry-fix-issues"
+source = "getsentry/sentry-for-ai"
+
+[[skills]]
+name = "sentry-code-review"
+source = "getsentry/sentry-for-ai"
+
+[[skills]]
+name = "sentry-pr-code-review"
+source = "getsentry/sentry-for-ai"
+
+[[skills]]
+name = "create-java-pr"
+source = "path:.agents/skills/create-java-pr"
+
+[[skills]]
+name = "test"
+source = "path:.agents/skills/test"
+
+[[skills]]
+name = "btrace-perfetto"
+source = "path:.agents/skills/btrace-perfetto"
+
+[[skills]]
+name = "check-code-attribution"
+source = "path:.agents/skills/check-code-attribution"
diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts
index 8abe9f55283..bba758f9b79 100644
--- a/build-logic/build.gradle.kts
+++ b/build-logic/build.gradle.kts
@@ -7,5 +7,19 @@ repositories {
}
dependencies {
+ implementation(libs.animalsniffer.gradle.plugin)
implementation(libs.spotlessLib)
}
+
+gradlePlugin {
+ plugins {
+ register("sentryAnimalSniffer") {
+ id = "io.sentry.animalsniffer"
+ implementationClass = "io.sentry.gradle.SentryAnimalSnifferPlugin"
+ }
+ register("sentryAnimalSnifferAndroid") {
+ id = "io.sentry.animalsniffer.android"
+ implementationClass = "io.sentry.gradle.SentryAnimalSnifferAndroidPlugin"
+ }
+ }
+}
diff --git a/build-logic/src/main/kotlin/io.sentry.javadoc.aggregate.gradle.kts b/build-logic/src/main/kotlin/io.sentry.javadoc.aggregate.gradle.kts
index e06cb677319..8fde556d751 100644
--- a/build-logic/src/main/kotlin/io.sentry.javadoc.aggregate.gradle.kts
+++ b/build-logic/src/main/kotlin/io.sentry.javadoc.aggregate.gradle.kts
@@ -1,11 +1,9 @@
import io.sentry.gradle.AggregateJavadoc
import org.gradle.api.attributes.Category
import org.gradle.api.attributes.LibraryElements
-import org.gradle.kotlin.dsl.creating
-import org.gradle.kotlin.dsl.getValue
import org.gradle.kotlin.dsl.named
-val javadocPublisher by configurations.creating {
+val javadocPublisher = configurations.create("javadocPublisher") {
isCanBeConsumed = false
isCanBeResolved = true
attributes {
@@ -15,7 +13,7 @@ val javadocPublisher by configurations.creating {
}
subprojects {
- javadocPublisher.dependencies.add(dependencies.create(this))
+ javadocPublisher.dependencies.add(rootProject.dependencies.project(path))
}
val javadocCollection = javadocPublisher.incoming.artifactView { lenient(true) }.files
diff --git a/build-logic/src/main/kotlin/io.sentry.javadoc.gradle.kts b/build-logic/src/main/kotlin/io.sentry.javadoc.gradle.kts
index 7eb796a02ff..21f81fec36a 100644
--- a/build-logic/src/main/kotlin/io.sentry.javadoc.gradle.kts
+++ b/build-logic/src/main/kotlin/io.sentry.javadoc.gradle.kts
@@ -1,4 +1,4 @@
-val javadocConfig: Configuration by configurations.creating {
+val javadocConfig: Configuration = configurations.create("javadocConfig") {
isCanBeResolved = false
isCanBeConsumed = true
diff --git a/build-logic/src/main/kotlin/io.sentry.systemtest.gradle.kts b/build-logic/src/main/kotlin/io.sentry.systemtest.gradle.kts
new file mode 100644
index 00000000000..a21079e1336
--- /dev/null
+++ b/build-logic/src/main/kotlin/io.sentry.systemtest.gradle.kts
@@ -0,0 +1,38 @@
+import io.sentry.gradle.SystemTestExtension
+import org.gradle.api.tasks.ClasspathNormalizer
+
+val systemTest = extensions.create("sentrySystemTest")
+
+// The sample system tests launch the packaged app (war/shadowJar/bootJar) from build/libs as a
+// separate process, so the archive is a real input even though it is not on the test classpath.
+// Agent-based samples are additionally launched with -javaagent:, another runtime
+// input not on the classpath. See test/system-test-runner.py.
+tasks.matching { it.name == "systemTest" }.configureEach {
+ val archiveTask =
+ listOf("war", "shadowJar", "bootJar").firstOrNull { it in tasks.names }
+ ?: throw GradleException(
+ "io.sentry.systemtest is applied to $path but none of war/shadowJar/bootJar " +
+ "exist to provide the launched app archive for the systemTest task"
+ )
+ // Declaring the archive as an input also wires the dependency on its producing task.
+ inputs
+ .files(tasks.named(archiveTask))
+ .withPropertyName("appArchive")
+ .withNormalizer(ClasspathNormalizer::class.java)
+
+ if (systemTest.usesOpenTelemetryAgent.get()) {
+ // The runner builds the agent and launches the app with -javaagent before invoking this task,
+ // so the agent jar is tracked for content only (by path, no cross-project task dependency): a
+ // change to it makes systemTest out of date even though it runs outside the test JVM.
+ val version = providers.gradleProperty("versionName").get()
+ inputs
+ .files(
+ rootProject.layout.projectDirectory.file(
+ "sentry-opentelemetry/sentry-opentelemetry-agent/build/libs/" +
+ "sentry-opentelemetry-agent-$version.jar"
+ )
+ )
+ .withPropertyName("openTelemetryAgent")
+ .withNormalizer(ClasspathNormalizer::class.java)
+ }
+}
diff --git a/build-logic/src/main/kotlin/io/sentry/gradle/SentryAnimalSnifferPlugin.kt b/build-logic/src/main/kotlin/io/sentry/gradle/SentryAnimalSnifferPlugin.kt
new file mode 100644
index 00000000000..f1bc2bafcf7
--- /dev/null
+++ b/build-logic/src/main/kotlin/io/sentry/gradle/SentryAnimalSnifferPlugin.kt
@@ -0,0 +1,57 @@
+package io.sentry.gradle
+
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.artifacts.MinimalExternalModuleDependency
+import org.gradle.api.artifacts.VersionCatalogsExtension
+import org.gradle.api.provider.ListProperty
+import ru.vyarus.gradle.plugin.animalsniffer.AnimalSniffer
+
+abstract class SentryAnimalSnifferExtension {
+ abstract val ignoredClasses: ListProperty
+ abstract val excludedClasses: ListProperty
+
+ fun ignoreClasses(vararg classes: String) {
+ ignoredClasses.addAll(*classes)
+ }
+
+ fun mainExcludes(vararg excludes: String) {
+ excludedClasses.addAll(*excludes)
+ }
+}
+
+class SentryAnimalSnifferPlugin : Plugin {
+ override fun apply(project: Project) {
+ project.pluginManager.apply("ru.vyarus.animalsniffer")
+
+ val extension =
+ project.extensions.create("sentryAnimalSniffer", SentryAnimalSnifferExtension::class.java)
+
+ project.addSignatureDependency("java8-signature")
+
+ project.tasks.named("animalsnifferMain", AnimalSniffer::class.java).configure {
+ ignoreClasses = ignoreClasses + extension.ignoredClasses.get()
+ exclude(extension.excludedClasses.get())
+ }
+
+ project.tasks.named("check").configure { dependsOn("animalsnifferMain") }
+ }
+}
+
+class SentryAnimalSnifferAndroidPlugin : Plugin {
+ override fun apply(project: Project) {
+ project.pluginManager.apply(SentryAnimalSnifferPlugin::class.java)
+
+ project.addSignatureDependency("gummy-bears-api21")
+ }
+}
+
+private fun Project.addSignatureDependency(libraryName: String) {
+ val libs = extensions.getByType(VersionCatalogsExtension::class.java).named("libs")
+ dependencies.add("signature", signatureNotation(libs.findLibrary(libraryName).get().get()))
+}
+
+private fun signatureNotation(dependency: MinimalExternalModuleDependency): String {
+ val module = "${dependency.module.group}:${dependency.module.name}"
+ return "$module:${dependency.versionConstraint.requiredVersion}@signature"
+}
diff --git a/build-logic/src/main/kotlin/io/sentry/gradle/SystemTestExtension.kt b/build-logic/src/main/kotlin/io/sentry/gradle/SystemTestExtension.kt
new file mode 100644
index 00000000000..9111ce17b1f
--- /dev/null
+++ b/build-logic/src/main/kotlin/io/sentry/gradle/SystemTestExtension.kt
@@ -0,0 +1,17 @@
+package io.sentry.gradle
+
+import org.gradle.api.provider.Property
+
+/** Configuration for the `io.sentry.systemtest` convention plugin. */
+abstract class SystemTestExtension {
+ /**
+ * Set to `true` for samples that the system-test runner launches with the Sentry OpenTelemetry
+ * Java agent (`-javaagent`). The agent jar is then tracked as a `systemTest` input so the task
+ * re-runs when the agent changes, even though it is started outside the test JVM.
+ */
+ abstract val usesOpenTelemetryAgent: Property
+
+ init {
+ usesOpenTelemetryAgent.convention(false)
+ }
+}
diff --git a/build.gradle.kts b/build.gradle.kts
index 23eaca36936..a663628b467 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -3,19 +3,15 @@ import com.vanniktech.maven.publish.JavadocJar
import com.vanniktech.maven.publish.MavenPublishBaseExtension
import groovy.util.Node
import io.gitlab.arturbosch.detekt.extensions.DetektExtension
-import kotlinx.kover.gradle.plugin.dsl.KoverReportExtension
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.api.tasks.testing.logging.TestLogEvent
plugins {
`java-library`
alias(libs.plugins.spotless) apply false
- jacoco
alias(libs.plugins.detekt)
`maven-publish`
alias(libs.plugins.binary.compatibility.validator)
- alias(libs.plugins.jacoco.android) apply false
- alias(libs.plugins.kover) apply false
alias(libs.plugins.vanniktech.maven.publish) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.multiplatform) apply false
@@ -30,6 +26,7 @@ plugins {
alias(libs.plugins.gradle.versions) apply false
alias(libs.plugins.spring.dependency.management) apply false
id("io.sentry.javadoc.aggregate")
+ alias(libs.plugins.sentry) apply false
}
buildscript {
@@ -76,6 +73,7 @@ apiValidation {
"sentry-samples-spring-boot-4",
"sentry-samples-spring-boot-4-opentelemetry",
"sentry-samples-spring-boot-4-opentelemetry-noagent",
+ "sentry-samples-spring-boot-4-otlp",
"sentry-samples-spring-boot-4-webflux",
"sentry-samples-ktor-client",
"sentry-uitest-android",
@@ -83,14 +81,18 @@ apiValidation {
"sentry-uitest-android-critical",
"test-app-plain",
"test-app-sentry",
- "sentry-samples-netflix-dgs"
+ "test-app-size",
+ "sentry-samples-netflix-dgs",
+ "sentry-samples-console-otlp",
+ "sentry-test-support",
+ "sentry-system-test-support"
)
)
}
allprojects {
group = Config.Sentry.group
- version = properties[Config.Sentry.versionNameProp].toString()
+ version = providers.gradleProperty(Config.Sentry.versionNameProp).get()
description = Config.Sentry.description
tasks {
withType().configureEach {
@@ -101,13 +103,9 @@ allprojects {
TestLogEvent.PASSED,
TestLogEvent.FAILED
)
-
- // Cap JVM args per test
- minHeapSize = "256m"
- maxHeapSize = "2g"
}
withType().configureEach {
- options.compilerArgs.addAll(arrayOf("-Xlint:all", "-Werror", "-Xlint:-classfile", "-Xlint:-processing", "-Xlint:-try"))
+ options.compilerArgs.addAll(arrayOf("-Xlint:all", "-Werror", "-Xlint:-classfile", "-Xlint:-processing", "-Xlint:-try", "-Xlint:-options"))
}
}
}
@@ -115,44 +113,6 @@ allprojects {
subprojects {
apply { plugin("io.sentry.spotless") }
- val jacocoAndroidModules = listOf(
- "sentry-android-core",
- "sentry-android-fragment",
- "sentry-android-navigation",
- "sentry-android-ndk",
- "sentry-android-sqlite",
- "sentry-android-replay",
- "sentry-android-timber"
- )
- if (jacocoAndroidModules.contains(name)) {
- afterEvaluate {
- jacoco {
- toolVersion = "0.8.10"
- }
-
- tasks.withType().configureEach {
- configure {
- isIncludeNoLocationClasses = true
- excludes = listOf("jdk.internal.*")
- }
- }
- }
- }
-
- val koverKmpModules = listOf("sentry-compose")
- if (koverKmpModules.contains(name)) {
- afterEvaluate {
- configure {
- androidReports("release") {
- xml {
- // Change the report file name so the Codecov Github action can find it
- setReportFile(project.layout.buildDirectory.file("reports/kover/report.xml").get().asFile)
- }
- }
- }
- }
- }
-
plugins.withId(Config.QualityPlugins.detektPlugin) {
configure {
buildUponDefaultConfig = true
@@ -161,7 +121,7 @@ subprojects {
}
}
- if (!this.name.contains("sample") && !this.name.contains("integration-tests") && this.name != "sentry-system-test-support" && this.name != "sentry-test-support" && this.name != "sentry-android-distribution") {
+ if (!this.name.contains("sample") && !this.name.contains("integration-tests") && this.name != "sentry-system-test-support" && this.name != "sentry-test-support") {
apply()
apply()
@@ -208,9 +168,28 @@ subprojects {
}
}
- afterEvaluate {
- apply()
+ // AGP 9 defaults Android modules to Java 11. Pin the published library modules back
+ // to Java 8 so their bytecode stays consumable by Java 8 projects, mirroring the
+ // java-library pin above.
+ plugins.withId("com.android.library") {
+ configure {
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+
+ // AGP 9 defaults the AAR metadata minCompileSdk to the library's compileSdk,
+ // which would force every consumer onto that compile SDK. Pin it to our minSdk
+ // so consumers remain free to compile against any SDK we support, as before.
+ defaultConfig {
+ aarMetadata { minCompileSdk = libs.versions.minSdk.get().toInt() }
+ }
+ }
+ }
+ apply()
+
+ afterEvaluate {
configure {
assignAarTypes()
}
@@ -245,21 +224,17 @@ tasks.register("buildForCodeQL") {
}
.forEach { proj ->
if (proj.plugins.hasPlugin("com.android.library")) {
- this.dependsOn(proj.tasks.findByName("compileReleaseUnitTestSources"))
+ proj.tasks.findByName("compileReleaseUnitTestSources")?.let { testTask ->
+ this.dependsOn(testTask)
+ }
} else {
- this.dependsOn(proj.tasks.findByName("testClasses"))
+ proj.tasks.findByName("testClasses")?.let { testTask ->
+ this.dependsOn(testTask)
+ }
}
}
}
-// Workaround for https://youtrack.jetbrains.com/issue/IDEA-316081/Gradle-8-toolchain-error-Toolchain-from-executable-property-does-not-match-toolchain-from-javaLauncher-property-when-different
-gradle.taskGraph.whenReady {
- val task = this.allTasks.find { it.name.endsWith(".main()") } as? JavaExec
- task?.let {
- it.setExecutable(it.javaLauncher.get().executablePath.asFile.absolutePath)
- }
-}
-
/*
* Adapted from https://github.com/androidx/androidx/blob/c799cba927a71f01ea6b421a8f83c181682633fb/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt#L524-L549
*
diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt
index 8f373df14dd..09d2869988b 100644
--- a/buildSrc/src/main/java/Config.kt
+++ b/buildSrc/src/main/java/Config.kt
@@ -1,8 +1,6 @@
-import java.math.BigDecimal
-
object Config {
- val AGP = System.getenv("VERSION_AGP") ?: "8.6.0"
+ val AGP = System.getenv("VERSION_AGP") ?: "9.2.1"
val kotlinStdLib = "stdlib-jdk8"
val kotlinStdLibVersionAndroid = "1.9.24"
val kotlinTestJunit = "test-junit"
@@ -14,8 +12,10 @@ object Config {
object Android {
val abiFilters = listOf("x86", "armeabi-v7a", "x86_64", "arm64-v8a")
+ // Debug variants are disabled everywhere. Unit tests run against the release
+ // variant, so building the debug variant would only add overhead.
fun shouldSkipDebugVariant(name: String?): Boolean {
- return System.getenv("CI")?.toBoolean() ?: false && name == "debug"
+ return name == "debug"
}
}
@@ -37,11 +37,6 @@ object Config {
}
object QualityPlugins {
- object Jacoco {
- // TODO [POTEL] add tests and restore
- val minimumCoverage = BigDecimal.valueOf(0.1)
- }
-
// this can be removed when we upgrade to Gradle 8, which allows us to use a getter for the plugin ID
val detektPlugin = "io.gitlab.arturbosch.detekt"
}
@@ -64,6 +59,8 @@ object Config {
val SENTRY_SPRING_BOOT_4_STARTER_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.spring-boot-4-starter"
val SENTRY_OPENTELEMETRY_BOOTSTRAP_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.opentelemetry.bootstrap"
val SENTRY_OPENTELEMETRY_CORE_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.opentelemetry.core"
+ val SENTRY_OPENTELEMETRY_OTLP_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.opentelemetry.otlp"
+ val SENTRY_OPENTELEMETRY_OTLP_SPRING_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.opentelemetry.otlp-spring"
val SENTRY_OPENTELEMETRY_AGENT_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.opentelemetry.agent"
val SENTRY_OPENTELEMETRY_AGENTLESS_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.opentelemetry.agentless"
val SENTRY_OPENTELEMETRY_AGENTLESS_SPRING_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.opentelemetry.agentless-spring"
@@ -75,14 +72,20 @@ object Config {
val SENTRY_GRAPHQL_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.graphql"
val SENTRY_GRAPHQL_CORE_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.graphql-core"
val SENTRY_GRAPHQL22_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.graphql22"
+ val SENTRY_JCACHE_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.jcache"
val SENTRY_QUARTZ_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.quartz"
val SENTRY_JDBC_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.jdbc"
+ val SENTRY_KAFKA_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.kafka"
+ val SENTRY_OPENFEATURE_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.openfeature"
+ val SENTRY_LAUNCHDARKLY_SERVER_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.launchdarkly-server"
+ val SENTRY_LAUNCHDARKLY_ANDROID_SDK_NAME = "$SENTRY_ANDROID_SDK_NAME.launchdarkly"
val SENTRY_SERVLET_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.servlet"
val SENTRY_SERVLET_JAKARTA_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.servlet.jakarta"
val SENTRY_COMPOSE_HELPER_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.compose.helper"
val SENTRY_OKHTTP_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.okhttp"
val SENTRY_REACTOR_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.reactor"
val SENTRY_KOTLIN_EXTENSIONS_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.kotlin-extensions"
+ val SENTRY_ASYNC_PROFILER_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.async-profiler"
val SENTRY_KTOR_CLIENT_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.ktor-client"
val group = "io.sentry"
val description = "SDK for sentry.io"
diff --git a/buildSrc/src/main/java/MergeSpringMetadataAction.kt b/buildSrc/src/main/java/MergeSpringMetadataAction.kt
new file mode 100644
index 00000000000..2df744924cb
--- /dev/null
+++ b/buildSrc/src/main/java/MergeSpringMetadataAction.kt
@@ -0,0 +1,292 @@
+import java.net.URI
+import java.nio.file.FileSystems
+import java.nio.file.Files
+import java.util.LinkedHashSet
+import java.util.zip.ZipFile
+import org.gradle.api.Action
+import org.gradle.api.Task
+import org.gradle.api.file.FileCollection
+import org.gradle.api.tasks.bundling.AbstractArchiveTask
+
+/**
+ * Patches a built shadow JAR by merging Spring metadata and service descriptor files from the
+ * runtime classpath into the final archive.
+ *
+ * Spring metadata files do not all share the same merge semantics, so this action merges
+ * `spring.factories` as list properties, `.imports` files as line-based metadata, and other Spring
+ * metadata as key/value properties. It also deduplicates service-provider configuration entries
+ * under `META-INF/services` so the flat executable JAR keeps the runtime registrations it needs.
+ */
+class MergeSpringMetadataAction(
+ private val runtimeClasspath: FileCollection,
+ private val springMetadataFiles: List,
+) : Action {
+ companion object {
+ val DEFAULT_SPRING_METADATA_FILES =
+ listOf(
+ "META-INF/spring.factories",
+ "META-INF/spring.handlers",
+ "META-INF/spring.schemas",
+ "META-INF/spring-autoconfigure-metadata.properties",
+ "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports",
+ "META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports",
+ )
+ }
+
+ override fun execute(task: Task) {
+ val archiveTask = task as AbstractArchiveTask
+ val jar = archiveTask.archiveFile.get().asFile
+ val runtimeJars = runtimeClasspath.files.filter { it.name.endsWith(".jar") }
+ val uri = URI.create("jar:${jar.toURI()}")
+
+ FileSystems.newFileSystem(uri, mapOf("create" to "false")).use { fs ->
+ springMetadataFiles.forEach { entryPath ->
+ val target = fs.getPath(entryPath)
+ val contents = mutableListOf()
+
+ if (Files.exists(target)) {
+ contents.add(Files.readString(target))
+ }
+
+ runtimeJars.forEach { depJar ->
+ try {
+ ZipFile(depJar).use { zip ->
+ val entry = zip.getEntry(entryPath)
+ if (entry != null) {
+ contents.add(zip.getInputStream(entry).bufferedReader().readText())
+ }
+ }
+ } catch (_: Exception) {
+ // Ignore non-zip files on the runtime classpath.
+ }
+ }
+
+ val merged =
+ when {
+ entryPath == "META-INF/spring.factories" -> mergeListProperties(contents)
+ entryPath.endsWith(".imports") -> mergeLineBasedMetadata(contents)
+ else -> mergeMapProperties(contents)
+ }
+
+ if (merged.isNotEmpty()) {
+ if (target.parent != null) {
+ Files.createDirectories(target.parent)
+ }
+ Files.write(target, merged.toByteArray())
+ }
+ }
+
+ val serviceEntries = linkedSetOf()
+
+ runtimeJars.forEach { depJar ->
+ try {
+ ZipFile(depJar).use { zip ->
+ val entries = zip.entries()
+ while (entries.hasMoreElements()) {
+ val entry = entries.nextElement()
+ if (!entry.isDirectory && entry.name.startsWith("META-INF/services/")) {
+ serviceEntries.add(entry.name)
+ }
+ }
+ }
+ } catch (_: Exception) {
+ // Ignore non-zip files on the runtime classpath.
+ }
+ }
+
+ serviceEntries.forEach { entryPath ->
+ val providers = LinkedHashSet()
+ val target = fs.getPath(entryPath)
+
+ if (Files.exists(target)) {
+ Files.newBufferedReader(target).useLines { lines ->
+ lines.forEach { line ->
+ val provider = line.trim()
+ if (provider.isNotEmpty() && !provider.startsWith("#")) {
+ providers.add(provider)
+ }
+ }
+ }
+ }
+
+ runtimeJars.forEach { depJar ->
+ try {
+ ZipFile(depJar).use { zip ->
+ val entry = zip.getEntry(entryPath)
+ if (entry != null) {
+ zip.getInputStream(entry).bufferedReader().useLines { lines ->
+ lines.forEach { line ->
+ val provider = line.trim()
+ if (provider.isNotEmpty() && !provider.startsWith("#")) {
+ providers.add(provider)
+ }
+ }
+ }
+ }
+ }
+ } catch (_: Exception) {
+ // Ignore non-zip files on the runtime classpath.
+ }
+ }
+
+ if (providers.isNotEmpty()) {
+ if (target.parent != null) {
+ Files.createDirectories(target.parent)
+ }
+ Files.write(target, providers.joinToString(separator = "\n", postfix = "\n").toByteArray())
+ }
+ }
+ }
+ }
+
+ private fun mergeLineBasedMetadata(contents: List): String {
+ val lines = LinkedHashSet()
+
+ contents.forEach { content ->
+ content.lineSequence().forEach { rawLine ->
+ val line = rawLine.trim()
+ if (line.isNotEmpty() && !line.startsWith("#")) {
+ lines.add(line)
+ }
+ }
+ }
+
+ return if (lines.isEmpty()) "" else lines.joinToString(separator = "\n", postfix = "\n")
+ }
+
+ private fun mergeMapProperties(contents: List): String {
+ val merged = linkedMapOf()
+
+ contents.forEach { content ->
+ parseProperties(content).forEach { (key, value) ->
+ merged[key] = value
+ }
+ }
+
+ return if (merged.isEmpty()) {
+ ""
+ } else {
+ merged.entries.joinToString(separator = "\n", postfix = "\n") { (key, value) -> "$key=$value" }
+ }
+ }
+
+ private fun mergeListProperties(contents: List): String {
+ val merged = linkedMapOf>()
+
+ contents.forEach { content ->
+ parseProperties(content).forEach { (key, value) ->
+ val values = merged.getOrPut(key) { LinkedHashSet() }
+ value
+ .split(',')
+ .map(String::trim)
+ .filter(String::isNotEmpty)
+ .forEach(values::add)
+ }
+ }
+
+ return if (merged.isEmpty()) {
+ ""
+ } else {
+ merged.entries.joinToString(separator = "\n", postfix = "\n") { (key, values) ->
+ "$key=${values.joinToString(separator = ",")}"
+ }
+ }
+ }
+
+ private fun parseProperties(content: String): List> {
+ val logicalLines = mutableListOf()
+ val current = StringBuilder()
+
+ content.lineSequence().forEach { rawLine ->
+ val line = rawLine.trim()
+ if (current.isEmpty() && (line.isEmpty() || line.startsWith("#") || line.startsWith("!"))) {
+ return@forEach
+ }
+
+ val normalized = if (current.isEmpty()) line else line.trimStart()
+ current.append(
+ if (endsWithContinuation(rawLine)) normalized.dropLast(1) else normalized,
+ )
+
+ if (!endsWithContinuation(rawLine)) {
+ logicalLines.add(current.toString())
+ current.setLength(0)
+ }
+ }
+
+ if (current.isNotEmpty()) {
+ logicalLines.add(current.toString())
+ }
+
+ return logicalLines.map { line ->
+ val separatorIndex = findSeparatorIndex(line)
+ if (separatorIndex < 0) {
+ line to ""
+ } else {
+ val keyEnd = trimTrailingWhitespace(line, separatorIndex)
+ val valueStart = findValueStart(line, separatorIndex)
+ line.substring(0, keyEnd) to line.substring(valueStart).trim()
+ }
+ }
+ }
+
+ private fun endsWithContinuation(line: String): Boolean {
+ var backslashCount = 0
+
+ for (index in line.length - 1 downTo 0) {
+ if (line[index] == '\\') {
+ backslashCount++
+ } else {
+ break
+ }
+ }
+
+ return backslashCount % 2 == 1
+ }
+
+ private fun findSeparatorIndex(line: String): Int {
+ var backslashCount = 0
+
+ line.forEachIndexed { index, char ->
+ if (char == '\\') {
+ backslashCount++
+ } else {
+ val isEscaped = backslashCount % 2 == 1
+ if (!isEscaped && (char == '=' || char == ':' || char.isWhitespace())) {
+ return index
+ }
+ backslashCount = 0
+ }
+ }
+
+ return -1
+ }
+
+ private fun trimTrailingWhitespace(line: String, endExclusive: Int): Int {
+ var end = endExclusive
+
+ while (end > 0 && line[end - 1].isWhitespace()) {
+ end--
+ }
+
+ return end
+ }
+
+ private fun findValueStart(line: String, separatorIndex: Int): Int {
+ var valueStart = separatorIndex
+
+ while (valueStart < line.length && line[valueStart].isWhitespace()) {
+ valueStart++
+ }
+
+ if (valueStart < line.length && (line[valueStart] == '=' || line[valueStart] == ':')) {
+ valueStart++
+ }
+
+ while (valueStart < line.length && line[valueStart].isWhitespace()) {
+ valueStart++
+ }
+
+ return valueStart
+ }
+}
diff --git a/buildSrc/src/main/java/Publication.kt b/buildSrc/src/main/java/Publication.kt
index 0aa717a5630..d545e6e32dc 100644
--- a/buildSrc/src/main/java/Publication.kt
+++ b/buildSrc/src/main/java/Publication.kt
@@ -7,10 +7,13 @@ private object Consts {
val taskRegex = Regex("(.*)DistZip")
}
+private fun Project.versionName(): String =
+ providers.gradleProperty("versionName").get()
+
// configure distZip tasks for multiplatform
fun DistributionContainer.configureForMultiplatform(project: Project) {
val sep = File.separator
- val version = project.properties["versionName"].toString()
+ val version = project.versionName()
val name = project.name
this.maybeCreate("android").contents {
@@ -69,7 +72,7 @@ fun DistributionContainer.configureForMultiplatform(project: Project) {
fun DistributionContainer.configureForJvm(project: Project) {
val sep = File.separator
- val version = project.properties["versionName"].toString()
+ val version = project.versionName()
val name = project.name
this.getByName("main").contents {
diff --git a/codecov.yml b/codecov.yml
deleted file mode 100644
index 9b1af61b4c6..00000000000
--- a/codecov.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-comment: no
-codecov:
- require_ci_to_pass: no
- max_report_age: off
-
-coverage:
- status:
- project:
- default:
- target: 78%
- threshold: 4%
- patch: off
- range: 78...100
- precision: 3
- round: down
-
-ignore:
- - "**/src/test/*"
- - "sentry-android-integration-tests/*"
- - "sentry-system-test-support/*"
- - "sentry-test-support/*"
- - "sentry-samples/*"
diff --git a/develop-docs/README.md b/develop-docs/README.md
new file mode 100644
index 00000000000..b9c2c7913ca
--- /dev/null
+++ b/develop-docs/README.md
@@ -0,0 +1,149 @@
+# Develop Docs
+
+This folder holds internal developer documentation for the Sentry Java/Android SDK:
+architecture notes, feature deep-dives, design decisions, and cross-module concepts
+that don't belong in the public [Sentry docs](https://docs.sentry.io) or in inline
+code comments.
+
+If you are documenting **how** or **why** something works for the people who maintain
+this SDK, it goes here. If you are documenting **how to use** the SDK for end users,
+it belongs in the public docs instead.
+
+## Rules
+
+These rules keep the docs consistent, easy to navigate, and easy to grep.
+
+### Directory structure
+
+Documents live in **subdirectories**, one level per level of grouping. Directories are
+cheap: reach for a new one as soon as a topic has more than one document, or as soon as you
+can name the group.
+
+Every document sits under one of these top-level categories:
+
+- `general/` — cross-cutting topics (e.g. `general/architecture.md`, `general/pipeline.md`)
+- `feature/` — a specific SDK feature (e.g. `feature/errors/`, `feature/profiling/`)
+- `integration/` — a specific integration or module (e.g. `integration/opentelemetry/`, `integration/spring/`)
+- `platform/` — platform-specific concerns (e.g. `platform/android/`, `platform/jvm/`)
+- `process/` — team processes and workflows (e.g. `process/release.md`)
+
+Add a new category only when an existing one clearly does not fit, and keep the list above
+up to date.
+
+Below the category, nest by topic and then by sub-topic. A fully grown feature might look
+like this:
+
+```text
+develop-docs/
+ README.md
+ general/
+ pipeline.md
+ feature/
+ profiling/
+ overview.md
+ perfetto.md
+ anr.md
+ symbolication/
+ deobfuscation.md
+```
+
+- Give a directory an `overview.md` once it holds several documents, and link to its
+ siblings from there.
+- Do not create a directory that will only ever hold one document — put the document
+ directly in the category (`general/pipeline.md`, not `general/pipeline/pipeline.md`).
+
+### File naming
+
+- File names are **lowercase**, except for this `README.md`, which GitHub renders as the
+ folder's landing page.
+- Use **dashes** (`-`) as separators, never underscores or spaces. For example, use
+ `session-replay.md`, not `session_replay.md` or `Session Replay.md`.
+- Use the `.md` extension for all text documents.
+- **Do not repeat the path in the file name.** The directories carry the namespace, so the
+ file name only needs the part that distinguishes it from its siblings:
+ `feature/profiling/perfetto.md`, not `feature/profiling/perfetto-profiling.md`.
+- Choose short, descriptive names (`feature/replay/masking.md`, not
+ `feature/replay/how-masking-works.md`).
+
+### Images and other assets
+
+- When a document embeds images (or other binary assets), store them in an **`assets/`
+ folder next to the document**. Documents in the same directory share it:
+
+ ```text
+ develop-docs/
+ feature/
+ profiling/
+ perfetto.md
+ assets/
+ pipeline.png
+ overview.svg
+ ```
+
+- Reference assets with **relative paths**: ``.
+- Asset file names follow the same rules as documents: lowercase, dashes, descriptive.
+- Prefer **vector formats** (SVG) for diagrams and screenshots where practical
+- Prefer **Mermaid** over a static image whenever a diagram can be expressed as one
+ (see below) — it lives in the document, is versioned as text, and is easy to update.
+
+### Writing style
+
+- Write in the **present tense** and the **active voice**. Describe how the system
+ behaves now ("The transport retries failed envelopes"), not how it will or did behave.
+ This way there's no need to update the docs once a feature ships.
+- Keep one **top-level `# ` heading** per document (the title), and nest sections with
+ `##`, `###`, etc. Do not skip heading levels.
+- Keep documents focused on a **single topic**. Split large topics into several documents
+ in a shared directory and link between them rather than growing one giant file.
+- Use fenced **code blocks with a language identifier** (```kotlin `,
+ ` ```bash `) so syntax highlighting works.
+- Prefer Kotlin snippets over Java.
+- When referencing code, link to the file with a **relative path** (e.g.
+ `../../../sentry/src/main/java/io/sentry/Sentry.java`) rather than pasting large excerpts
+ that fall out of date. Count the `../` from the document's own directory.
+- Avoid pinning content to a specific SDK version or date unless it is genuinely
+ version-specific; keep docs evergreen.
+- Cross-link related documents with relative links (e.g.
+ `[the ingestion pipeline](../../general/pipeline.md)`).
+
+### Structuring a feature document
+
+Most feature documents answer the same four questions, and following that order makes them
+easier to compare and to keep current:
+
+1. **Surface area** — where and when the SDK collects the data.
+2. **Collection** — how the SDK collects it.
+3. **Format** — what the collected data looks like on the wire.
+4. **Pipeline** — how the backend ingests, stores, and serves it.
+
+Do not restate (4) in every document. Describe the shared path once in
+[general/pipeline.md](general/pipeline.md) and cover only the deviations a feature
+introduces. Omit any of the four that a feature does not have, and keep each as high-level
+as the topic allows so the document stays true for longer.
+
+### Diagrams with Mermaid
+
+- Prefer [Mermaid](https://mermaid.js.org/) for diagrams. It renders directly on GitHub
+ and lives in the document as text, so it versions and reviews like code.
+- Embed a Mermaid diagram in a fenced block tagged `mermaid`:
+
+ ````markdown
+ ```mermaid
+ flowchart LR
+ Event[SentryEvent] --> Processor[EventProcessors]
+ Processor --> Transport
+ Transport --> Sentry[(Sentry)]
+ ```
+ ````
+
+- For complex diagrams, include a link to the [Mermaid Live Editor](https://mermaid.live/)
+ so reviewers can iterate quickly.
+- Fall back to static images (stored per the asset rules above) if mermaid is not practicable.
+
+## Adding a new document
+
+1. Pick the right top-level category (or introduce a new one and document it above).
+2. Pick or create the topic directory below it.
+3. Create the document, naming it for what distinguishes it from its siblings.
+4. If the directory now holds several documents, add or update its `overview.md`.
+5. If the document embeds assets, put them in an `assets/` folder next to it.
diff --git a/develop-docs/feature/profiling/perfetto.md b/develop-docs/feature/profiling/perfetto.md
new file mode 100644
index 00000000000..d4168f8f2a4
--- /dev/null
+++ b/develop-docs/feature/profiling/perfetto.md
@@ -0,0 +1,232 @@
+# Perfetto profiling on Android
+
+This document describes how continuous profiling works on Android when the SDK
+captures traces through the OS-level [`android.os.ProfilingManager`](https://developer.android.com/reference/android/os/ProfilingManager)
+API (available on API 35+), and how a captured **profile chunk** flows all the way
+from the device to a downloadable profile in Sentry.
+
+## What Perfetto is
+
+[Perfetto](https://perfetto.dev/) is Google's tracing framework for Android and Linux, and
+the tooling Android itself is instrumented with. Its
+[callstack sampler](https://perfetto.dev/docs/getting-started/cpu-profiling) interrupts the
+app at a fixed frequency, records the native and Java call stacks of the running threads,
+and writes them to a binary `.pftrace` file (a serialized
+[Perfetto protobuf](https://perfetto.dev/docs/reference/trace-packet-proto)).
+Starting with Android 15, apps can request such traces at
+runtime via `ProfilingManager` without root or `adb`, which is what makes on-device
+continuous profiling possible.
+
+Useful Perfetto references:
+
+- Perfetto docs: https://perfetto.dev/docs/
+- CPU profiling with Perfetto: https://perfetto.dev/docs/getting-started/cpu-profiling
+- Trace format (`TracePacket` proto): https://perfetto.dev/docs/reference/trace-packet-proto
+- Perfetto UI (to open a downloaded `.pftrace`): https://ui.perfetto.dev/
+
+## Pipeline overview
+
+Profile chunks travel the standard ingestion path described in
+[general/pipeline.md](../../general/pipeline.md) — SDK envelope,
+[Relay](https://develop.sentry.dev/ingestion/relay/) (Sentry's ingestion proxy), Kafka, a
+monolith processing task, then storage and a read API. Read that first; the rest of this
+document covers only where Perfetto deviates from it.
+
+The deviations are:
+
+- The envelope item carries **JSON and raw binary in one payload**, subdivided by a
+ `meta_length` header rather than base64-encoding the trace ([details](#envelope-format-and-the-meta_length-header)).
+- Relay **converts** the Perfetto trace into the existing Sample v2 profile format, and
+ additionally **keeps the raw `.pftrace`** in the object store so it can be downloaded
+ later ([details](#relay-getsentryrelay)).
+
+```mermaid
+flowchart TD
+ subgraph device["Android device — sentry-java"]
+ PM[android.os.ProfilingManager]
+ PP[PerfettoProfiler]
+ PCP[PerfettoContinuousProfiler]
+ PC[ProfileChunk]
+ ENV["Envelope item [JSON metadata][raw .pftrace] header: meta_length"]
+ PM --> PP --> PCP --> PC --> ENV
+ end
+
+ subgraph relay["Relay (processing mode)"]
+ SPLIT[Split payload at meta_length]
+ CONV[Convert Perfetto → Sample v2]
+ OS1[Upload raw .pftrace to object store]
+ KAFKA[["Kafka topic: profiles ProfileChunkKafkaMessage (Sample v2 + attachment stored_id)"]]
+ SPLIT --> CONV --> KAFKA
+ SPLIT --> OS1
+ end
+
+ subgraph monolith["Monolith — getsentry/sentry"]
+ TASK[process_profile_task]
+ SYM[Symbolicate / deobfuscate]
+ VR[vroomrs: parse + normalize]
+ OS2[(Object store)]
+ SNUBA[(Snuba: function metrics)]
+ DB[(ProfileChunkAttachment row)]
+ TASK --> SYM --> VR
+ VR --> OS2
+ VR --> SNUBA
+ TASK --> DB
+ end
+
+ ENV -->|envelope| relay
+ KAFKA --> TASK
+ OS1 -.stored_id.-> DB
+ VROOM[getsentry/vroom serve + merge flamegraphs]
+ OS2 --> VROOM
+ SNUBA --> VROOM
+```
+
+## SDK (getsentry/sentry-java)
+
+On API 35+, [`AndroidOptionsInitializer`](../../../sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java)
+wires up `PerfettoContinuousProfiler` automatically. On older devices the SDK falls back
+to the legacy `Debug`-based [`AndroidContinuousProfiler`](../../../sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java),
+gated by the `enableLegacyProfiling` option (manifest key
+`io.sentry.profiling.enable-legacy-profiling`, defaults to `true`). Only **continuous
+profiling** is supported on the Perfetto path — transaction-based and app-start profiling
+are not.
+
+### Capturing chunks
+
+Continuous profiling emits a stream of independent [`ProfileChunk`](../../../sentry/src/main/java/io/sentry/ProfileChunk.java)s
+rather than one profile per transaction. `PerfettoContinuousProfiler` drives a chained
+loop: each chunk runs for `MAX_CHUNK_DURATION_MILLIS` (60s) via `PerfettoProfiler`, which
+calls `ProfilingManager.requestProfiling(PROFILING_TYPE_STACK_SAMPLING, …)` at
+`PROFILING_FREQUENCY_HZ` (101 Hz). When a chunk's trace file is ready, a new chunk starts,
+so profiling runs continuously.
+
+A chunk keeps a stable `profilerId` across the session and a per-chunk `chunkId`. When the
+OS produces the trace file, the profiler builds a `ProfileChunk` tagged with the Perfetto
+content type:
+
+```kotlin
+ProfileChunk.Builder(profilerId, chunkId, measurements, traceFile, timestamp, ProfileChunk.PLATFORM_ANDROID)
+ .setContentType(ProfileChunk.CONTENT_TYPE_PERFETTO) // "application/x-perfetto-trace"
+ .build()
+```
+
+The chunk is captured via `scopes.captureProfileChunk(...)` and sent as its own envelope
+with item type [`SentryItemType.ProfileChunk`](../../../sentry/src/main/java/io/sentry/SentryItemType.java)
+(wire name `profile_chunk`).
+
+### Envelope format and the `meta_length` header
+
+A legacy chunk base64-encodes its trace into the `ProfileChunk` JSON. A Perfetto chunk is
+much larger, so [`SentryClient`](../../../sentry/src/main/java/io/sentry/SentryClient.java) instead
+routes it through the new `SentryEnvelopeItem.fromPerfettoProfileChunk(...)` factory, which
+avoids base64 by sending the raw binary alongside the JSON.
+
+The trick is a single envelope **item** whose payload concatenates the JSON metadata and
+the raw `.pftrace` bytes with **no delimiter**:
+
+```text
+[ProfileChunk JSON bytes][raw .pftrace binary bytes]
+```
+
+A new `meta_length` property on the [envelope item header](../../../sentry/src/main/java/io/sentry/SentryEnvelopeItemHeader.java)
+tells the server where the JSON ends and the binary begins. The standard envelope item
+structure (header line + newline + payload) is unchanged; `meta_length` simply subdivides
+the payload:
+
+```text
+{"type":"profile_chunk","content_type":"application/x-perfetto-trace","filename":"…","length":,"meta_length":}
+
+```
+
+- `length` — total payload size (JSON + binary), as for any envelope item.
+- `meta_length` — byte length of the JSON prefix. It is only known after the payload is
+ serialized, so the header computes it lazily (via a `Callable`) and omits the
+ field entirely for non-Perfetto items, keeping the change backward compatible.
+
+## Relay (getsentry/relay)
+
+In processing mode Relay:
+
+1. **Splits** the compound item payload at `meta_length` into `(metadata JSON, raw profile)`
+ and reads `content_type: "perfetto"` from the metadata.
+2. **Converts** the binary Perfetto trace into the existing **Sample v2** profile JSON
+ format (`relay_profiling::expand_perfetto(...)`, backed by a checked-in subset of the
+ Perfetto protobuf definitions).
+3. **Uploads** the raw `.pftrace` blob to object store (usecase `profiles`, keyed per
+ org/project, with an attachment-retention TTL).
+4. **Produces** a `ProfileChunkKafkaMessage` to the `profiles` Kafka topic. The message
+ carries the expanded Sample v2 JSON as `payload` plus an `attachments` array, where each
+ attachment records:
+ - `name` (e.g. `profile.perfetto`),
+ - `content_type` (e.g. `application/x-perfetto-trace`),
+ - `stored_id` — the object store key of the uploaded raw blob.
+
+```json
+{
+ "organization_id": 1,
+ "project_id": 42,
+ "received": 1720000000,
+ "retention_days": 30,
+ "payload": "",
+ "attachments": [
+ {
+ "name": "profile.perfetto",
+ "content_type": "application/x-perfetto-trace",
+ "stored_id": ""
+ }
+ ]
+}
+```
+
+The monolith later uses `stored_id` to fetch the raw trace back.
+
+## Monolith (getsentry/sentry)
+
+`process_profile_task` (in `src/sentry/profiles/task.py`) consumes the `profiles` topic.
+Because Relay already converted the trace to Sample v2, the task treats a Perfetto chunk
+like any other: deobfuscate, hand it to `vroomrs` to parse and normalize
+(`vroomrs.profile_chunk_from_json_str(...)`), compress and store it, and emit function
+metrics to Snuba.
+
+The Perfetto-specific step is the last one: for each attachment on the message the task
+persists a lightweight **`ProfileChunkAttachment`** row — `project_id`, `profiler_id`,
+`chunk_id`, `name`, `content_type`, and the `stored_id` object store key. The row exists so
+the raw trace can be downloaded by ID without exposing the `stored_id`.
+
+Flamegraphs themselves are served by `getsentry/vroom`, which reads the stored chunks and
+the Snuba-indexed metadata and merges several chunks into one flamegraph. The endpoint
+lives in the monolith and passes the request through.
+
+### Perfetto format dispatch (vroom / vroomrs)
+
+Older Android SDKs emit the legacy Android trace format tagged as a "faulty" `version=2`,
+and the pipeline historically keyed off the platform rather than the version. To
+distinguish legacy from Sample v2 chunks, `ProfileChunk` carries a dedicated `version`
+field, and both `vroom` and `vroomrs` now dispatch on it instead of the platform:
+
+- Version `""` or `2.android-trace` → legacy Android trace format.
+- Any other version → Sample v2.
+
+## Downloading a Perfetto profile
+
+The monolith exposes two feature-gated endpoints:
+
+- **List attachments** — `GET /organizations/{org}/profiling/chunk-attachments/`
+ (`sentry-api-0-organization-profiling-chunk-attachments`). Requires a `project` and
+ `profiler_id`; resolves the visible `chunk_id`s (same logic as the flamegraph) and returns
+ the matching `ProfileChunkAttachment` metadata.
+- **Download** — `GET /projects/{org}/{project}/profiling/chunks/{profiler_id}/{chunk_id}/attachments/{attachment_id}/?download`
+ (`sentry-api-0-project-profiling-chunk-attachment`). The `?download` param is required; it
+ streams the raw blob back from object store via the stored `stored_id`. Access requires
+ the org's configured attachments role, analogous to generic event attachments.
+
+In the flamegraph UI, a toolbar button (added for continuous profiles when the feature is
+enabled and at least one attachment exists) lists and provides a way to download these traces.
+
+## References
+
+- SDK: [sentry-java#5251](https://github.com/getsentry/sentry-java/pull/5251) — Android `ProfilingManager` (Perfetto) support
+- Relay: [#5659](https://github.com/getsentry/relay/pull/5659), [#5932](https://github.com/getsentry/relay/pull/5932), [#6099](https://github.com/getsentry/relay/pull/6099), [#6102](https://github.com/getsentry/relay/pull/6102) — Perfetto parsing, pipeline, and object-store routing
+- vroom: [#672](https://github.com/getsentry/vroom/pull/672) — version dispatch for Android trace profiles
+- vroomrs: [#93](https://github.com/getsentry/vroomrs/pull/93) — accept Android profiles in Sample v2 format
+- Monolith: [sentry#118029](https://github.com/getsentry/sentry/pull/118029) (chunk attachments + endpoints), [sentry#118071](https://github.com/getsentry/sentry/pull/118071) (flamegraph download button)
diff --git a/develop-docs/general/pipeline.md b/develop-docs/general/pipeline.md
new file mode 100644
index 00000000000..6cb0d87f696
--- /dev/null
+++ b/develop-docs/general/pipeline.md
@@ -0,0 +1,121 @@
+# Ingestion pipeline
+
+This document describes the path data takes from an SDK to a rendered view in Sentry. It
+covers the parts of different payload types, like errors, transactions, logs, replays and
+profile chunks.
+
+## Per data category
+
+Every payload takes the same four hops — SDK, Relay, a consumer in the monolith, and a read
+API — but the topics, processing tasks, and stores differ per category. The diagrams below
+show three of them; the hops themselves are described further down.
+
+### Errors
+
+```mermaid
+flowchart LR
+ SDK["SDK captures + batches"] -->|envelope| RELAY
+ RELAY["Relay authenticate, normalize, route"] -->|ingest-events| KAFKA[["Kafka"]]
+ RELAY -.->|attachments, minidumps| OS[("Object store")]
+ KAFKA --> TASK["save_event task"]
+ TASK --> SYM["Symbolicator symbolicate, deobfuscate"]
+ SYM --> TASK
+ TASK --> NS[("Nodestore full event body")]
+ TASK --> SNUBA[("Snuba searchable columns")]
+ TASK --> PG[("Postgres Group / GroupHash rows")]
+ NS --> READ["Read path monolith API"]
+ SNUBA --> READ
+ PG --> READ
+ OS --> READ
+```
+
+### Transactions
+
+```mermaid
+flowchart LR
+ SDK["SDK captures spans"] -->|envelope| RELAY
+ RELAY["Relay normalize, dynamic sampling, metric extraction"] -->|ingest-transactions| KAFKA[["Kafka"]]
+ KAFKA --> CONSUMER["Transaction consumer"]
+ CONSUMER --> SNUBA[("Snuba transactions + spans")]
+ CONSUMER --> NS[("Nodestore full transaction body")]
+ SNUBA --> READ["Read path monolith API"]
+ NS --> READ
+```
+
+### Profile chunks
+
+```mermaid
+flowchart LR
+ SDK["SDK captures profile chunks"] -->|envelope| RELAY
+ RELAY["Relay convert Perfetto → Sample v2"] -->|profiles| KAFKA[["Kafka"]]
+ RELAY -.->|raw .pftrace blob| OS[("Object store")]
+ KAFKA --> TASK["process_profile_task"]
+ TASK --> VRS["vroomrs parse + normalize"]
+ VRS --> OS
+ VRS --> SNUBA[("Snuba function metrics")]
+ TASK --> PG[("Postgres ProfileChunkAttachment rows")]
+ OS --> VROOM["vroom serve + merge flamegraphs"]
+ SNUBA --> VROOM
+ VROOM --> READ["Read path monolith API"]
+ PG --> READ
+```
+
+## The hops
+
+### 1. SDK
+
+The SDK captures data and wraps it in an [envelope](https://develop.sentry.dev/sdk/data-model/envelopes/):
+a JSON header followed by one or more items, each with its own header declaring a `type`,
+a `length`, and optionally a `content_type`. The envelope is POSTed to the project's
+`/api/{project_id}/envelope/` endpoint.
+
+The item `type` is what routes the payload through everything downstream, so adding a new
+kind of data means adding an item type, not a new endpoint. Item payloads are usually JSON;
+binary payloads are allowed and are preferable to base64-encoding a large blob into JSON.
+
+### 2. Relay
+
+[Relay](https://github.com/getsentry/relay) is Sentry's ingestion proxy — it sits between
+the SDK and the rest of the infrastructure and is the first service to inspect a payload.
+See the [Relay chapter in develop docs](https://develop.sentry.dev/ingestion/relay/) for
+the full picture.
+
+Relay authenticates the DSN, applies quotas and rate limits, filters and normalizes the
+payload, and forwards it. Two behaviours matter when designing a new payload type:
+
+- Relay may **convert** a payload into a different format before publishing it, so the
+ format the SDK sends and the format the backend consumes are not necessarily the same.
+ Whatever Relay publishes is the contract every downstream service depends on.
+- Relay runs in two modes. Only **processing mode** (the one Sentry operates) talks to
+ Kafka and the object store; a self-hosted Relay in proxy mode just forwards envelopes
+ upstream.
+
+Relay publishes to a **Kafka topic per data category**. Payloads too large to sit
+comfortably in a Kafka message are uploaded to the **object store** instead, and the
+message carries a reference to the stored blob rather than the bytes themselves. Event
+attachments (minidumps, screenshots, view hierarchies) work this way, and so does the raw
+`.pftrace` blob of a Perfetto profile chunk: Relay uploads the trace and puts only its
+`stored_id` object store key on the Kafka message.
+
+### 3. Consumers and processing
+
+Each topic is consumed by the monolith ([getsentry/sentry](https://github.com/getsentry/sentry)),
+which runs a processing task per message. This is where the work that needs Sentry-side
+state happens — symbolication and deobfuscation against uploaded debug files, enrichment,
+normalization, and quota accounting.
+
+A task typically writes to more than one store:
+
+- **Object store** — the payload itself, compressed. Cheap to keep, not queryable.
+- **Snuba** — the columns that need to be searched, aggregated, or listed.
+- **Postgres** — small metadata rows that the API needs to resolve a request, for example
+ a row per stored blob so it can be fetched by ID instead of by exposing its storage key.
+
+### 4. Read path
+
+The monolith serves the API endpoints. For some categories it does the work itself; for
+others it authorizes the request and proxies it to a dedicated service that owns the
+heavy read logic. Either way the endpoint is the public surface, and the storage keys and
+internal services stay behind it.
+
+See [feature/profiling/perfetto.md](../feature/profiling/perfetto.md) for a worked example.
diff --git a/devenv/config.ini b/devenv/config.ini
new file mode 100644
index 00000000000..b546b762c0c
--- /dev/null
+++ b/devenv/config.ini
@@ -0,0 +1,16 @@
+[devenv]
+minimum_version = 1.22.1
+
+[uv]
+darwin_arm64 = https://github.com/astral-sh/uv/releases/download/0.8.2/uv-aarch64-apple-darwin.tar.gz
+darwin_arm64_sha256 = 954d24634d5f37fa26c7af75eb79893d11623fc81b4de4b82d60d1ade4bfca22
+darwin_x86_64 = https://github.com/astral-sh/uv/releases/download/0.8.2/uv-x86_64-apple-darwin.tar.gz
+darwin_x86_64_sha256 = ae755df53c8c2c1f3dfbee6e3d2e00be0dfbc9c9b4bdffdb040b96f43678b7ce
+linux_arm64 = https://github.com/astral-sh/uv/releases/download/0.8.2/uv-aarch64-unknown-linux-gnu.tar.gz
+linux_arm64_sha256 = 27da35ef54e9131c2e305de67dd59a07c19257882c6b1f3cf4d8d5fbb8eaf4ca
+linux_x86_64 = https://github.com/astral-sh/uv/releases/download/0.8.2/uv-x86_64-unknown-linux-gnu.tar.gz
+linux_x86_64_sha256 = 6dcb28a541868a455aefb2e8d4a1283dd6bf888605a2db710f0530cec888b0ad
+# used for autoupdate
+# NOTE: if using uv-build as a build backend, you'll have to make sure the versions match
+version = 0.8.2
+
diff --git a/devenv/sync.py b/devenv/sync.py
new file mode 100644
index 00000000000..45e663cd99a
--- /dev/null
+++ b/devenv/sync.py
@@ -0,0 +1,23 @@
+from devenv import constants
+from devenv.lib import config, proc, uv
+import os
+
+def main(context: dict[str, str]) -> int:
+ reporoot = context["reporoot"]
+ cfg = config.get_repo(reporoot)
+
+ uv.install(
+ cfg["uv"]["version"],
+ cfg["uv"][constants.SYSTEM_MACHINE],
+ cfg["uv"][f"{constants.SYSTEM_MACHINE}_sha256"],
+ reporoot,
+ )
+
+ # reporoot/.venv is the default venv location
+ print(f"syncing .venv ...")
+ if not os.path.exists(".venv"):
+ proc.run(("uv", "venv", "--seed"))
+ proc.run(("uv", "sync", "--frozen", "--quiet"))
+
+ return 0
+
diff --git a/gradle.properties b/gradle.properties
index 90a57b64f42..e9bfc0e8156 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -4,14 +4,19 @@ org.gradle.caching=true
org.gradle.parallel=true
org.gradle.configureondemand=true
org.gradle.configuration-cache=true
+org.gradle.configuration-cache.parallel=true
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
# AndroidX required by AGP >= 3.6.x
android.useAndroidX=true
+# AGP 9+ migration opt-outs until we remove kotlin-android plugin and adopt built-in Kotlin.
+android.builtInKotlin=false
+android.newDsl=false
+android.experimental.lint.version=9.2.1
# Release information
-versionName=8.22.0
+versionName=8.53.0
# Override the SDK name on native crashes on Android
sentryAndroidSdkName=sentry.native.android
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 60c163373a7..bb4d18c7a0e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,19 +1,23 @@
[versions]
+animalsniffer = "2.0.1"
apollo = "2.5.9"
androidxLifecycle = "2.2.0"
androidxNavigation = "2.4.2"
androidxTestCore = "1.7.0"
androidxCompose = "1.6.3"
+asyncProfiler = "4.4"
+camerax = "1.4.0"
composeCompiler = "1.5.14"
coroutines = "1.6.1"
espresso = "3.7.0"
feign = "11.6"
-jacoco = "0.8.7"
+gummyBears = "0.12.0"
+java8Signature = "1.0"
jackson = "2.18.3"
jetbrainsCompose = "1.6.11"
-kotlin = "2.2.0"
-kotlinSpring7 = "2.2.0"
+kotlin = "2.3.21"
kotlin-compatible-version = "1.9"
+ksp = "2.3.9"
ktorClient = "3.0.0"
logback = "1.2.9"
log4j2 = "2.20.0"
@@ -21,32 +25,39 @@ nopen = "1.0.1"
# see https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-compatibility-and-versioning.html#kotlin-compatibility
# see https://developer.android.com/jetpack/androidx/releases/compose-kotlin
okhttp = "4.9.2"
-otel = "1.51.0"
-otelInstrumentation = "2.17.0"
-otelInstrumentationAlpha = "2.17.0-alpha"
+openfeature = "1.18.2"
+otel = "1.63.0"
+otelAlpha = "1.63.0-alpha"
+otelInstrumentation = "2.29.0"
+otelInstrumentationAlpha = "2.29.0-alpha"
# check https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/dependencyManagement/build.gradle.kts#L49 for release version above to find a compatible version
-otelSemanticConventions = "1.34.0"
-otelSemanticConventionsAlpha = "1.34.0-alpha"
+otelSemanticConventions = "1.42.0"
+otelSemanticConventionsAlpha = "1.42.0-alpha"
retrofit = "2.9.0"
+room2 = "2.8.4"
+room3 = "3.0.0-rc01"
+sagp = "6.13.0"
+sqlite = "2.6.2"
+sqliteRc = "2.7.0-rc01" # Required by Room3 3.0.0-rc*
slf4j = "1.7.30"
+spotless = "8.8.0"
springboot2 = "2.7.18"
springboot3 = "3.5.0"
-springboot4 = "4.0.0-M3"
+springboot4 = "4.1.0"
+sqldelight = "2.3.2"
+
# Android
-targetSdk = "34"
-compileSdk = "34"
+targetSdk = "37"
+compileSdk = "37"
minSdk = "21"
-spotless = "7.0.4"
-gummyBears = "0.12.0"
[plugins]
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" }
-kotlin-spring7 = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlinSpring7" }
-kotlin-jvm-spring7 = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlinSpring7" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
+ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
buildconfig = { id = "com.github.gmazzo.buildconfig", version = "5.6.5" }
dokka = { id = "org.jetbrains.dokka", version = "2.0.0" }
dokka-javadoc = { id = "org.jetbrains.dokka-javadoc", version = "2.0.0" }
@@ -55,17 +66,18 @@ errorprone = { id = "net.ltgt.errorprone", version = "3.0.1" }
gradle-versions = { id = "com.github.ben-manes.versions", version = "0.42.0" }
spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
detekt = { id = "io.gitlab.arturbosch.detekt", version = "1.23.8" }
-jacoco-android = { id = "com.mxalbert.gradle.jacoco-android", version = "0.2.0" }
-kover = { id = "org.jetbrains.kotlinx.kover", version = "0.7.3" }
vanniktech-maven-publish = { id = "com.vanniktech.maven.publish", version = "0.30.0" }
-springboot2 = { id = "org.springframework.boot", version.ref = "springboot2" }
springboot3 = { id = "org.springframework.boot", version.ref = "springboot3" }
springboot4 = { id = "org.springframework.boot", version.ref = "springboot4" }
-spring-dependency-management = { id = "io.spring.dependency-management", version = "1.0.11.RELEASE" }
+spring-dependency-management = { id = "io.spring.dependency-management", version = "1.1.7" }
+sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
gretty = { id = "org.gretty", version = "4.0.0" }
-animalsniffer = { id = "ru.vyarus.animalsniffer", version = "2.0.1" }
+animalsniffer = { id = "ru.vyarus.animalsniffer", version.ref = "animalsniffer" }
+sentry = { id = "io.sentry.android.gradle", version.ref = "sagp"}
+shadow = { id = "com.gradleup.shadow", version = "9.4.1" }
[libraries]
+animalsniffer-gradle-plugin = { module = "ru.vyarus:gradle-animalsniffer-plugin", version.ref = "animalsniffer" }
apache-httpclient = { module = "org.apache.httpcomponents.client5:httpclient5", version = "5.0.4" }
apollo2-coroutines = { module = "com.apollographql.apollo:apollo-coroutines-support", version.ref = "apollo" }
apollo2-runtime = { module = "com.apollographql.apollo:apollo-runtime", version.ref = "apollo" }
@@ -76,11 +88,13 @@ androidx-annotation = { module = "androidx.annotation:annotation", version = "1.
androidx-activity-compose = { module = "androidx.activity:activity-compose", version = "1.8.2" }
androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "androidxCompose" }
androidx-compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout", version.ref = "androidxCompose" }
-androidx-compose-material3 = { module = "androidx.compose.material3:material3", version = "1.2.1" }
+androidx-compose-material3 = { module = "androidx.compose.material3:material3", version = "1.4.0" }
+androidx-compose-material-icons-core = { module = "androidx.compose.material:material-icons-core", version="1.7.8" }
+androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version="1.7.8" }
androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "androidxCompose" }
# Note: don't change without testing forwards compatibility
-androidx-compose-ui-replay = { module = "androidx.compose.ui:ui", version = "1.5.0" }
-androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version = "2.1.3" }
+androidx-compose-ui-replay = { module = "androidx.compose.ui:ui", version = "1.10.2" }
+androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version = "2.2.1" }
androidx-core = { module = "androidx.core:core", version = "1.3.2" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version = "1.7.0" }
androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version = "1.3.5" }
@@ -88,8 +102,20 @@ androidx-lifecycle-common-java8 = { module = "androidx.lifecycle:lifecycle-commo
androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "androidxLifecycle" }
androidx-navigation-runtime = { module = "androidx.navigation:navigation-runtime", version.ref = "androidxNavigation" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidxNavigation" }
-androidx-sqlite = { module = "androidx.sqlite:sqlite", version = "2.5.2" }
+androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room2" }
+androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room2" }
+androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room2" }
+androidx-room3-compiler = { module = "androidx.room3:room3-compiler", version.ref = "room3" }
+androidx-room3-runtime = { module = "androidx.room3:room3-runtime", version.ref = "room3" }
+androidx-sqlite = { module = "androidx.sqlite:sqlite", version.ref = "sqlite" }
+androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqliteRc" }
+androidx-sqlite-framework = { module = "androidx.sqlite:sqlite-framework", version.ref = "sqliteRc" }
androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version = "1.2.1" }
+androidx-browser = { module = "androidx.browser:browser", version = "1.8.0" }
+async-profiler = { module = "tools.profiler:async-profiler", version.ref = "asyncProfiler" }
+async-profiler-jfr-converter = { module = "tools.profiler:jfr-converter", version.ref = "asyncProfiler" }
+caffeine = { module = "com.github.ben-manes.caffeine:caffeine" }
+caffeine-jcache = { module = "com.github.ben-manes.caffeine:jcache", version = "3.2.0" }
coil-compose = { module = "io.coil-kt:coil-compose", version = "2.6.0" }
commons-compress = {module = "org.apache.commons:commons-compress", version = "1.25.0"}
context-propagation = { module = "io.micrometer:context-propagation", version = "1.1.0" }
@@ -105,41 +131,54 @@ jackson-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin"
jetbrains-annotations = { module = "org.jetbrains:annotations", version = "23.0.0" }
kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "kotlin" }
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
-kotlin-test-junit-spring7 = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlinSpring7" }
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorClient" }
ktor-client-java = { module = "io.ktor:ktor-client-java", version.ref = "ktorClient" }
+launchdarkly-android = { module = "com.launchdarkly:launchdarkly-android-client-sdk", version = "5.9.2" }
+launchdarkly-server = { module = "com.launchdarkly:launchdarkly-java-server-sdk", version = "7.13.4" }
log4j-api = { module = "org.apache.logging.log4j:log4j-api", version.ref = "log4j2" }
log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j2" }
leakcanary = { module = "com.squareup.leakcanary:leakcanary-android", version = "2.14" }
+lottie-compose = { module = "com.airbnb.android:lottie-compose", version = "6.7.1" }
logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
nopen-annotations = { module = "com.jakewharton.nopen:nopen-annotations", version.ref = "nopen" }
nopen-checker = { module = "com.jakewharton.nopen:nopen-checker", version.ref = "nopen" }
nullaway = { module = "com.uber.nullaway:nullaway", version = "0.9.5" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
+okhttp-bom = { module = "com.squareup.okhttp3:okhttp-bom", version.ref = "okhttp" }
+openfeature = { module = "dev.openfeature:sdk", version.ref = "openfeature" }
otel = { module = "io.opentelemetry:opentelemetry-sdk", version.ref = "otel" }
+otel-exporter-otlp = { module = "io.opentelemetry:opentelemetry-exporter-otlp", version.ref = "otel" }
+otel-exporter-logging = { module = "io.opentelemetry:opentelemetry-exporter-logging", version.ref = "otel" }
otel-extension-autoconfigure = { module = "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure", version.ref = "otel" }
otel-extension-autoconfigure-spi = { module = "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi", version.ref = "otel" }
+otel-bom = { module = "io.opentelemetry:opentelemetry-bom", version.ref = "otel" }
+otel-alpha-bom = { module = "io.opentelemetry:opentelemetry-bom-alpha", version.ref = "otelAlpha" }
otel-instrumentation-bom = { module = "io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom", version.ref = "otelInstrumentation" }
+otel-instrumentation-alpha-bom = { module = "io.opentelemetry.instrumentation:opentelemetry-instrumentation-bom-alpha", version.ref = "otelInstrumentationAlpha" }
otel-javaagent = { module = "io.opentelemetry.javaagent:opentelemetry-javaagent", version.ref = "otelInstrumentation" }
otel-javaagent-tooling = { module = "io.opentelemetry.javaagent:opentelemetry-javaagent-tooling", version.ref = "otelInstrumentationAlpha" }
otel-javaagent-extension-api = { module = "io.opentelemetry.javaagent:opentelemetry-javaagent-extension-api", version.ref = "otelInstrumentationAlpha" }
otel-semconv = { module = "io.opentelemetry.semconv:opentelemetry-semconv", version.ref = "otelSemanticConventions" }
otel-semconv-incubating = { module = "io.opentelemetry.semconv:opentelemetry-semconv-incubating", version.ref = "otelSemanticConventionsAlpha" }
p6spy = { module = "p6spy:p6spy", version = "3.9.1" }
+epitaph = { module = "com.abovevacant:epitaph", version = "0.1.1" }
+jcache = { module = "javax.cache:cache-api", version = "1.1.1" }
quartz = { module = "org.quartz-scheduler:quartz", version = "2.3.0" }
reactor-core = { module = "io.projectreactor:reactor-core", version = "3.5.3" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
retrofit-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
-sentry-native-ndk = { module = "io.sentry:sentry-native-ndk", version = "0.10.1" }
+sentry-native-ndk = { module = "io.sentry:sentry-native-ndk", version = "0.16.3" }
servlet-api = { module = "javax.servlet:javax.servlet-api", version = "3.1.0" }
servlet-jakarta-api = { module = "jakarta.servlet:jakarta.servlet-api", version = "6.1.0" }
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
slf4j-jdk14 = { module = "org.slf4j:slf4j-jdk14", version.ref = "slf4j" }
slf4j2-api = { module = "org.slf4j:slf4j-api", version = "2.0.5" }
spotlessLib = { module = "com.diffplug.spotless:com.diffplug.spotless.gradle.plugin", version.ref = "spotless"}
+springboot2-bom = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "springboot2" }
springboot-starter = { module = "org.springframework.boot:spring-boot-starter", version.ref = "springboot2" }
+spring-graphql = { module = "org.springframework.graphql:spring-graphql", version = "1.0.6" }
springboot-starter-graphql = { module = "org.springframework.boot:spring-boot-starter-graphql", version.ref = "springboot2" }
springboot-starter-quartz = { module = "org.springframework.boot:spring-boot-starter-quartz", version.ref = "springboot2" }
springboot-starter-test = { module = "org.springframework.boot:spring-boot-starter-test", version.ref = "springboot2" }
@@ -150,6 +189,7 @@ springboot-starter-aop = { module = "org.springframework.boot:spring-boot-starte
springboot-starter-security = { module = "org.springframework.boot:spring-boot-starter-security", version.ref = "springboot2" }
springboot-starter-jdbc = { module = "org.springframework.boot:spring-boot-starter-jdbc", version.ref = "springboot2" }
springboot-starter-actuator = { module = "org.springframework.boot:spring-boot-starter-actuator", version.ref = "springboot2" }
+springboot-starter-cache = { module = "org.springframework.boot:spring-boot-starter-cache", version.ref = "springboot2" }
springboot3-otel = { module = "io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter", version.ref = "otelInstrumentation" }
springboot3-starter = { module = "org.springframework.boot:spring-boot-starter", version.ref = "springboot3" }
springboot3-starter-graphql = { module = "org.springframework.boot:spring-boot-starter-graphql", version.ref = "springboot3" }
@@ -162,7 +202,13 @@ springboot3-starter-aop = { module = "org.springframework.boot:spring-boot-start
springboot3-starter-security = { module = "org.springframework.boot:spring-boot-starter-security", version.ref = "springboot3" }
springboot3-starter-jdbc = { module = "org.springframework.boot:spring-boot-starter-jdbc", version.ref = "springboot3" }
springboot3-starter-actuator = { module = "org.springframework.boot:spring-boot-starter-actuator", version.ref = "springboot3" }
+springboot3-starter-cache = { module = "org.springframework.boot:spring-boot-starter-cache", version.ref = "springboot3" }
+spring-kafka2 = { module = "org.springframework.kafka:spring-kafka", version = "2.8.11" }
+spring-kafka3 = { module = "org.springframework.kafka:spring-kafka", version = "3.3.5" }
+spring-kafka4 = { module = "org.springframework.kafka:spring-kafka" }
+kafka-clients = { module = "org.apache.kafka:kafka-clients", version = "3.8.1" }
springboot4-otel = { module = "io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter", version.ref = "otelInstrumentation" }
+springboot4-resttestclient = { module = "org.springframework.boot:spring-boot-resttestclient", version.ref = "springboot4" }
springboot4-starter = { module = "org.springframework.boot:spring-boot-starter", version.ref = "springboot4" }
springboot4-starter-graphql = { module = "org.springframework.boot:spring-boot-starter-graphql", version.ref = "springboot4" }
springboot4-starter-quartz = { module = "org.springframework.boot:spring-boot-starter-quartz", version.ref = "springboot4" }
@@ -176,30 +222,43 @@ springboot4-starter-restclient = { module = "org.springframework.boot:spring-boo
springboot4-starter-webclient = { module = "org.springframework.boot:spring-boot-starter-webclient", version.ref = "springboot4" }
springboot4-starter-jdbc = { module = "org.springframework.boot:spring-boot-starter-jdbc", version.ref = "springboot4" }
springboot4-starter-actuator = { module = "org.springframework.boot:spring-boot-starter-actuator", version.ref = "springboot4" }
+springboot4-starter-cache = { module = "org.springframework.boot:spring-boot-starter-cache", version.ref = "springboot4" }
+springboot4-starter-kafka = { module = "org.springframework.boot:spring-boot-starter-kafka", version.ref = "springboot4" }
+sqldelight-android-driver = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
timber = { module = "com.jakewharton.timber:timber", version = "4.7.1" }
# Animalsniffer signature
gummy-bears-api21 = { module = "com.toasttab.android:gummy-bears-api-21", version.ref = "gummyBears" }
+java8-signature = { module = "org.codehaus.mojo.signature:java18", version.ref = "java8Signature" }
# tomcat libraries
tomcat-catalina = { module = "org.apache.tomcat:tomcat-catalina", version = "9.0.108" }
tomcat-embed-jasper = { module = "org.apache.tomcat.embed:tomcat-embed-jasper", version = "9.0.108" }
-tomcat-catalina-jakarta = { module = "org.apache.tomcat:tomcat-catalina", version = "11.0.10" }
-tomcat-embed-jasper-jakarta = { module = "org.apache.tomcat.embed:tomcat-embed-jasper", version = "11.0.10" }
+tomcat-catalina-jakarta = { module = "org.apache.tomcat:tomcat-catalina", version = "11.0.22" }
+tomcat-embed-jasper-jakarta = { module = "org.apache.tomcat.embed:tomcat-embed-jasper", version = "11.0.22" }
# test libraries
-androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version = "1.6.8" }
+androidx-benchmark-macro-junit4 = { module = "androidx.benchmark:benchmark-macro-junit4", version = "1.4.1" }
+androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version = "1.9.5" }
androidx-test-core = { module = "androidx.test:core", version.ref = "androidxTestCore" }
androidx-test-core-ktx = { module = "androidx.test:core-ktx", version.ref = "androidxTestCore" }
androidx-test-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" }
androidx-test-espresso-idling-resource = { module = "androidx.test.espresso:espresso-idling-resource", version.ref = "espresso" }
-androidx-test-ext-junit = { module = "androidx.test.ext:junit", version = "1.1.5" }
-androidx-test-orchestrator = { module = "androidx.test:orchestrator", version = "1.5.0" }
+androidx-test-ext-junit = { module = "androidx.test.ext:junit", version = "1.3.0" }
+androidx-test-orchestrator = { module = "androidx.test:orchestrator", version = "1.6.1" }
androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidxTestCore" }
-androidx-test-runner = { module = "androidx.test:runner", version = "1.6.2" }
+androidx-test-runner = { module = "androidx.test:runner", version = "1.7.0" }
awaitility-kotlin = { module = "org.awaitility:awaitility-kotlin", version = "4.1.1" }
awaitility-kotlin-spring7 = { module = "org.awaitility:awaitility-kotlin", version = "4.3.0" }
awaitility3-kotlin = { module = "org.awaitility:awaitility-kotlin", version = "3.1.6" }
+
+# CameraX dependencies
+camerax-core = { module = "androidx.camera:camera-core", version.ref = "camerax" }
+camerax-camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camerax" }
+camerax-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camerax" }
+camerax-view = { module = "androidx.camera:camera-view", version.ref = "camerax" }
+
+google-truth = { module = "com.google.truth:truth", version = "1.4.5" }
hsqldb = { module = "org.hsqldb:hsqldb", version = "2.6.1" }
javafaker = { module = "com.github.javafaker:javafaker", version = "1.0.2" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
@@ -210,4 +269,8 @@ mockito-inline = { module = "org.mockito:mockito-inline", version = "4.8.0" }
msgpack = { module = "org.msgpack:msgpack-core", version = "0.9.8" }
okhttp-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" }
okio = { module = "com.squareup.okio:okio", version = "1.13.0" }
-roboelectric = { module = "org.robolectric:robolectric", version = "4.14" }
+roboelectric = { module = "org.robolectric:robolectric", version = "4.15" }
+
+[bundles]
+androidx-room2 = ["androidx-room-runtime", "androidx-room-ktx"]
+androidx-sqlite-drivers = ["androidx-sqlite-bundled", "androidx-sqlite-framework"]
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 1b33c55baab..b1b8ef56b44 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index d4081da476b..a9db11550c6 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,7 +1,9 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
networkTimeout=10000
+retries=0
+retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index 23d15a93670..249efbb032c 100755
--- a/gradlew
+++ b/gradlew
@@ -1,7 +1,7 @@
#!/bin/sh
#
-# Copyright © 2015-2021 the original authors.
+# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@
##############################################################################
#
-# Gradle start up script for POSIX generated by Gradle.
+# gradlew start up script for POSIX generated by Gradle.
#
# Important for running:
#
@@ -29,7 +29,7 @@
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
-# ksh Gradle
+# ksh gradlew
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
@@ -57,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
-# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -114,7 +114,6 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
-CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
@@ -172,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
- CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -212,7 +210,6 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
- -classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
diff --git a/gradlew.bat b/gradlew.bat
index db3a6ac207e..a51ec4f5886 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -19,12 +19,12 @@
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
-@rem Gradle startup script for Windows
+@rem gradlew startup script for Windows
@rem
@rem ##########################################################################
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
+@rem Set local scope for the variables, and ensure extensions are enabled
+setlocal EnableExtensions
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@@ -51,7 +51,7 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
-goto fail
+"%COMSPEC%" /c exit 1
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
@@ -65,30 +65,18 @@ echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
-goto fail
+"%COMSPEC%" /c exit 1
:execute
@rem Setup the command line
-set CLASSPATH=
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+@rem Execute gradlew
+@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
+@rem which allows us to clear the local environment before executing the java command
+endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
-:end
-@rem End local scope for the variables with windows NT shell
-if %ERRORLEVEL% equ 0 goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-set EXIT_CODE=%ERRORLEVEL%
-if %EXIT_CODE% equ 0 set EXIT_CODE=1
-if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
-exit /b %EXIT_CODE%
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
+:exitWithErrorLevel
+@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
+"%COMSPEC%" /c exit %ERRORLEVEL%
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 00000000000..55509e3912b
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,3 @@
+[project]
+name = "javasdk"
+version = "0.0.0"
diff --git a/requirements.txt b/requirements.txt
index 08623cdf27b..c573fa72259 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
certifi==2025.7.14
charset-normalizer==3.4.2
-idna==3.10
-requests==2.32.4
-urllib3==2.5.0
+idna==3.15
+requests==2.33.0
+urllib3==2.7.0
diff --git a/scripts/check-tombstone-proto-schema.sh b/scripts/check-tombstone-proto-schema.sh
new file mode 100755
index 00000000000..ecf492af7e8
--- /dev/null
+++ b/scripts/check-tombstone-proto-schema.sh
@@ -0,0 +1,219 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+TRACKED_COMMIT="981d145117e8992842cdddee555c57e60c7a220a"
+REMOTE_URL='https://android.googlesource.com/platform/system/core'
+REMOTE_BRANCH='main'
+PROTO_PATH='debuggerd/proto/tombstone.proto'
+GITILES_REF="refs/heads/${REMOTE_BRANCH}"
+GITILES_LOG_URL="${REMOTE_URL}/+log/${GITILES_REF}/${PROTO_PATH}?format=JSON"
+
+MODE=auto
+case "${1:-}" in
+ "")
+ ;;
+ --git-only)
+ MODE=git
+ ;;
+ --gitiles-only)
+ MODE=gitiles
+ ;;
+ *)
+ echo "Usage: $0 [--git-only|--gitiles-only]" >&2
+ exit 2
+ ;;
+esac
+
+TEMP_FILES=()
+TEMP_DIRS=()
+LATEST_COMMIT=""
+
+error() {
+ echo "ERROR: $*" >&2
+}
+
+show_output() {
+ local label=$1
+ local file=$2
+
+ if [ -s "$file" ]; then
+ echo "$label:" >&2
+ sed 's/^/ /' "$file" >&2
+ fi
+}
+
+require_command() {
+ local command_name=$1
+
+ if ! command -v "$command_name" >/dev/null 2>&1; then
+ error "Required command not found: $command_name"
+ return 1
+ fi
+}
+
+make_temp_file() {
+ local file
+ file=$(mktemp)
+ TEMP_FILES+=("$file")
+ printf '%s\n' "$file"
+}
+
+make_temp_dir() {
+ local dir
+ dir=$(mktemp -d)
+ TEMP_DIRS+=("$dir")
+ printf '%s\n' "$dir"
+}
+
+cleanup() {
+ local path
+
+ for path in "${TEMP_FILES[@]}"; do
+ rm -f "$path"
+ done
+
+ for path in "${TEMP_DIRS[@]}"; do
+ rm -rf "$path"
+ done
+}
+
+handle_unexpected_error() {
+ local exit_code=$?
+ error "Unexpected failure at line $1 while running: $2 (exit $exit_code)"
+ exit "$exit_code"
+}
+
+trap 'handle_unexpected_error "$LINENO" "$BASH_COMMAND"' ERR
+trap cleanup EXIT
+
+run_gitiles_check() {
+ local response_file
+ local stderr_file
+ local status
+
+ require_command curl || return 1
+ require_command jq || return 1
+
+ response_file=$(make_temp_file)
+ stderr_file=$(make_temp_file)
+
+ if curl -fsS "$GITILES_LOG_URL" -o "$response_file" 2>"$stderr_file"; then
+ :
+ else
+ status=$?
+ error "Failed to fetch Gitiles history from:"
+ error " $GITILES_LOG_URL"
+ error "curl exited with status $status."
+ show_output "curl output" "$stderr_file"
+ return 1
+ fi
+
+ if LATEST_COMMIT=$(tail -n +2 "$response_file" | jq -er '.log[0].commit' 2>"$stderr_file"); then
+ :
+ else
+ status=$?
+ error "Failed to parse the latest commit from the Gitiles response."
+ error "jq exited with status $status."
+ show_output "jq output" "$stderr_file"
+ echo "Response preview:" >&2
+ head -n 20 "$response_file" >&2
+ return 1
+ fi
+
+ if [ -z "$LATEST_COMMIT" ]; then
+ error "Gitiles response did not contain a commit hash."
+ echo "Response preview:" >&2
+ head -n 20 "$response_file" >&2
+ return 1
+ fi
+}
+
+run_git_check() {
+ local repo_dir
+ local stderr_file
+ local status
+
+ require_command git || return 1
+
+ repo_dir=$(make_temp_dir)
+ stderr_file=$(make_temp_file)
+
+ if GIT_TERMINAL_PROMPT=0 git clone \
+ --quiet \
+ --filter=blob:none \
+ --single-branch \
+ --branch "$REMOTE_BRANCH" \
+ --no-checkout \
+ "$REMOTE_URL" "$repo_dir" 2>"$stderr_file"; then
+ :
+ else
+ status=$?
+ error "Failed to clone $REMOTE_BRANCH from:"
+ error " $REMOTE_URL"
+ error "git clone exited with status $status."
+ show_output "git clone output" "$stderr_file"
+ return 1
+ fi
+
+ if LATEST_COMMIT=$(git -C "$repo_dir" log -n 1 --format=%H HEAD -- "$PROTO_PATH" 2>"$stderr_file"); then
+ :
+ else
+ status=$?
+ error "Failed to determine the latest commit that modified:"
+ error " $PROTO_PATH"
+ error "git log exited with status $status."
+ show_output "git log output" "$stderr_file"
+ return 1
+ fi
+
+ if [ -z "$LATEST_COMMIT" ]; then
+ error "Git history did not contain a commit for:"
+ error " $PROTO_PATH"
+ return 1
+ fi
+}
+
+report_result() {
+ echo "Tracked commit: $TRACKED_COMMIT"
+ echo "Latest commit: $LATEST_COMMIT"
+
+ if [ "$LATEST_COMMIT" != "$TRACKED_COMMIT" ]; then
+ echo "Schema has been updated! Latest: ${REMOTE_URL}/+/${LATEST_COMMIT}/${PROTO_PATH}"
+ exit 1
+ fi
+
+ echo "Schema is up to date."
+}
+
+case "$MODE" in
+ auto)
+ if run_gitiles_check; then
+ report_result
+ exit 0
+ fi
+
+ echo "Falling back to git-based check." >&2
+ if run_git_check; then
+ report_result
+ exit 0
+ fi
+
+ exit 1
+ ;;
+ gitiles)
+ if run_gitiles_check; then
+ report_result
+ exit 0
+ fi
+
+ exit 1
+ ;;
+ git)
+ if run_git_check; then
+ report_result
+ exit 0
+ fi
+
+ exit 1
+ ;;
+esac
diff --git a/scripts/update-gradle.sh b/scripts/update-gradle.sh
deleted file mode 100755
index c2bfe979224..00000000000
--- a/scripts/update-gradle.sh
+++ /dev/null
@@ -1,47 +0,0 @@
-#!/usr/bin/env bash
-set -euo pipefail
-
-cd $(dirname "$0")/../
-
-if [[ -n ${CI+x} ]]; then
- export JAVA_HOME=$JAVA_HOME_17_X64
-fi
-
-case $1 in
-get-version)
- # `./gradlew` shows some info on the first run, breaking the parsing in the next step.
- # Therefore, we run it once without checking any output.
- ./gradlew --version >/dev/null
- version="$(./gradlew --version | sed -E -n 's/.*Gradle +([0-9.]+).*/\1/p')"
-
- # Add trailing ".0" - gradlew outputs '7.1' instead of '7.1.0'
- if [[ "$version" =~ ^[0-9]\.[0-9]$ ]]; then
- version="$version.0"
- fi
-
- echo "v$version"
- ;;
-get-repo)
- echo "https://github.com/gradle/gradle.git"
- ;;
-set-version)
- version=$2
-
- # Remove leading "v"
- if [[ "$version" == v* ]]; then
- version="${version:1}"
- fi
-
- echo "Setting gradle version to '$version'"
-
- # This sets version to gradle-wrapper.properties.
- ./gradlew wrapper --gradle-version "$version"
-
- # Verify it works.
- ./gradlew --version
- ;;
-*)
- echo "Unknown argument $1"
- exit 1
- ;;
-esac
diff --git a/sentry-android-core/api/sentry-android-core.api b/sentry-android-core/api/sentry-android-core.api
index 5b416486576..65bf072f0a0 100644
--- a/sentry-android-core/api/sentry-android-core.api
+++ b/sentry-android-core/api/sentry-android-core.api
@@ -41,8 +41,9 @@ public final class io/sentry/android/core/ActivityLifecycleIntegration : android
}
public class io/sentry/android/core/AndroidContinuousProfiler : io/sentry/IContinuousProfiler, io/sentry/transport/RateLimiter$IRateLimitObserver {
- public fun (Lio/sentry/android/core/BuildInfoProvider;Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;Lio/sentry/ILogger;Ljava/lang/String;ILio/sentry/ISentryExecutorService;)V
+ public fun (Lio/sentry/android/core/BuildInfoProvider;Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;Lio/sentry/ILogger;Ljava/lang/String;ILio/sentry/util/LazyEvaluator$Evaluator;)V
public fun close (Z)V
+ public fun getChunkId ()Lio/sentry/protocol/SentryId;
public fun getProfilerId ()Lio/sentry/protocol/SentryId;
public fun getRootSpanCounter ()I
public fun isRunning ()Z
@@ -81,15 +82,39 @@ public final class io/sentry/android/core/AndroidLogger : io/sentry/ILogger {
public fun log (Lio/sentry/SentryLevel;Ljava/lang/Throwable;Ljava/lang/String;[Ljava/lang/Object;)V
}
+public final class io/sentry/android/core/AndroidLoggerBatchProcessor : io/sentry/logger/LoggerBatchProcessor, io/sentry/android/core/AppState$AppStateListener {
+ public fun (Lio/sentry/SentryOptions;Lio/sentry/ISentryClient;)V
+ public fun close (Z)V
+ public fun onBackground ()V
+ public fun onForeground ()V
+}
+
+public final class io/sentry/android/core/AndroidLoggerBatchProcessorFactory : io/sentry/logger/ILoggerBatchProcessorFactory {
+ public fun ()V
+ public fun create (Lio/sentry/SentryOptions;Lio/sentry/SentryClient;)Lio/sentry/logger/ILoggerBatchProcessor;
+}
+
public class io/sentry/android/core/AndroidMemoryCollector : io/sentry/IPerformanceSnapshotCollector {
public fun ()V
public fun collect (Lio/sentry/PerformanceCollectionData;)V
public fun setup ()V
}
+public final class io/sentry/android/core/AndroidMetricsBatchProcessor : io/sentry/metrics/MetricsBatchProcessor, io/sentry/android/core/AppState$AppStateListener {
+ public fun (Lio/sentry/SentryOptions;Lio/sentry/ISentryClient;)V
+ public fun close (Z)V
+ public fun onBackground ()V
+ public fun onForeground ()V
+}
+
+public final class io/sentry/android/core/AndroidMetricsBatchProcessorFactory : io/sentry/metrics/IMetricsBatchProcessorFactory {
+ public fun ()V
+ public fun create (Lio/sentry/SentryOptions;Lio/sentry/SentryClient;)Lio/sentry/metrics/IMetricsBatchProcessor;
+}
+
public class io/sentry/android/core/AndroidProfiler {
protected final field lock Lio/sentry/util/AutoClosableReentrantLock;
- public fun (Ljava/lang/String;ILio/sentry/android/core/internal/util/SentryFrameMetricsCollector;Lio/sentry/ISentryExecutorService;Lio/sentry/ILogger;)V
+ public fun (Ljava/lang/String;ILio/sentry/android/core/internal/util/SentryFrameMetricsCollector;Lio/sentry/util/LazyEvaluator$Evaluator;Lio/sentry/ILogger;)V
public fun close ()V
public fun endAndCollect (ZLjava/util/List;)Lio/sentry/android/core/AndroidProfiler$ProfileEndData;
public fun start ()Lio/sentry/android/core/AndroidProfiler$ProfileStartData;
@@ -128,13 +153,6 @@ public final class io/sentry/android/core/AnrIntegrationFactory {
public static fun create (Landroid/content/Context;Lio/sentry/android/core/BuildInfoProvider;)Lio/sentry/Integration;
}
-public final class io/sentry/android/core/AnrV2EventProcessor : io/sentry/BackfillingEventProcessor {
- public fun (Landroid/content/Context;Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/BuildInfoProvider;)V
- public fun getOrder ()Ljava/lang/Long;
- public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent;
- public fun process (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction;
-}
-
public class io/sentry/android/core/AnrV2Integration : io/sentry/Integration, java/io/Closeable {
public fun (Landroid/content/Context;)V
public fun close ()V
@@ -166,6 +184,30 @@ public final class io/sentry/android/core/AppLifecycleIntegration : io/sentry/In
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
}
+public final class io/sentry/android/core/AppStartExtension : io/sentry/IAppStartExtender {
+ public fun (Lio/sentry/android/core/performance/AppStartMetrics;)V
+ public fun clear ()V
+ public fun extendAppStart ()V
+ public fun finishExtendedAppStart ()V
+ public fun finishTransaction (Lio/sentry/SentryDate;)V
+ public fun getExtendedAppStartSpan ()Lio/sentry/ISpan;
+ public fun getExtendedEndTime ()Lio/sentry/SentryDate;
+ public fun isActive ()Z
+ public fun isExtended ()Z
+ public fun setData (Ljava/lang/String;Ljava/lang/Object;)V
+ public fun setExtendAppStartListener (Lio/sentry/android/core/AppStartExtension$ExtendAppStartListener;)V
+}
+
+public abstract interface class io/sentry/android/core/AppStartExtension$ExtendAppStartListener {
+ public abstract fun onExtendAppStartRequested ()Lio/sentry/android/core/AppStartExtension$ExtendedAppStart;
+}
+
+public final class io/sentry/android/core/AppStartExtension$ExtendedAppStart {
+ public final field span Lio/sentry/ISpan;
+ public final field transaction Lio/sentry/ITransaction;
+ public fun (Lio/sentry/ITransaction;Lio/sentry/ISpan;)V
+}
+
public final class io/sentry/android/core/AppState : java/io/Closeable {
public fun addAppStateListener (Lio/sentry/android/core/AppState$AppStateListener;)V
public fun close ()V
@@ -190,6 +232,13 @@ public final class io/sentry/android/core/AppState$LifecycleObserver : androidx/
public fun onStop (Landroidx/lifecycle/LifecycleOwner;)V
}
+public final class io/sentry/android/core/ApplicationExitInfoEventProcessor : io/sentry/BackfillingEventProcessor {
+ public fun (Landroid/content/Context;Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/BuildInfoProvider;)V
+ public fun getOrder ()Ljava/lang/Long;
+ public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent;
+ public fun process (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction;
+}
+
public final class io/sentry/android/core/BuildConfig {
public static final field BUILD_TYPE Ljava/lang/String;
public static final field DEBUG Z
@@ -244,6 +293,22 @@ public abstract class io/sentry/android/core/EnvelopeFileObserverIntegration : i
public final fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
}
+public final class io/sentry/android/core/FeedbackShakeIntegration : android/app/Application$ActivityLifecycleCallbacks, io/sentry/Integration, io/sentry/SentryFeedbackOptions$IShakeController, java/io/Closeable {
+ public fun (Landroid/app/Application;)V
+ public fun close ()V
+ public fun disableOnShake ()V
+ public fun enableOnShake ()V
+ public fun isOnShakeEnabled ()Z
+ public fun onActivityCreated (Landroid/app/Activity;Landroid/os/Bundle;)V
+ public fun onActivityDestroyed (Landroid/app/Activity;)V
+ public fun onActivityPaused (Landroid/app/Activity;)V
+ public fun onActivityResumed (Landroid/app/Activity;)V
+ public fun onActivitySaveInstanceState (Landroid/app/Activity;Landroid/os/Bundle;)V
+ public fun onActivityStarted (Landroid/app/Activity;)V
+ public fun onActivityStopped (Landroid/app/Activity;)V
+ public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
+}
+
public abstract interface class io/sentry/android/core/IDebugImagesLoader {
public abstract fun clearDebugImages ()V
public abstract fun loadDebugImages ()Ljava/util/List;
@@ -266,6 +331,19 @@ public final class io/sentry/android/core/LoadClass : io/sentry/util/LoadClass {
public fun loadClass (Ljava/lang/String;Lio/sentry/ILogger;)Ljava/lang/Class;
}
+public final class io/sentry/android/core/NativeEventCollector {
+ public fun (Lio/sentry/android/core/SentryAndroidOptions;)V
+ public fun collect ()V
+ public fun deleteNativeEventFile (Lio/sentry/android/core/NativeEventCollector$NativeEventData;)Z
+ public fun findAndRemoveMatchingNativeEvent (J)Lio/sentry/android/core/NativeEventCollector$NativeEventData;
+}
+
+public final class io/sentry/android/core/NativeEventCollector$NativeEventData {
+ public fun getEnvelope ()Lio/sentry/SentryEnvelope;
+ public fun getEvent ()Lio/sentry/SentryEvent;
+ public fun getFile ()Ljava/io/File;
+}
+
public final class io/sentry/android/core/NdkHandlerStrategy : java/lang/Enum {
public static final field SENTRY_HANDLER_STRATEGY_CHAIN_AT_START Lio/sentry/android/core/NdkHandlerStrategy;
public static final field SENTRY_HANDLER_STRATEGY_DEFAULT Lio/sentry/android/core/NdkHandlerStrategy;
@@ -287,8 +365,26 @@ public final class io/sentry/android/core/NetworkBreadcrumbsIntegration : io/sen
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
}
+public class io/sentry/android/core/PerfettoContinuousProfiler : io/sentry/IContinuousProfiler, io/sentry/transport/RateLimiter$IRateLimitObserver {
+ public fun (Lio/sentry/ILogger;Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;Lio/sentry/util/LazyEvaluator$Evaluator;Ljava/util/function/Supplier;)V
+ public fun close (Z)V
+ public fun getChunkId ()Lio/sentry/protocol/SentryId;
+ public fun getProfilerId ()Lio/sentry/protocol/SentryId;
+ public fun isRunning ()Z
+ public fun onRateLimitChanged (Lio/sentry/transport/RateLimiter;)V
+ public fun reevaluateSampling ()V
+ public fun startProfiler (Lio/sentry/ProfileLifecycle;Lio/sentry/TracesSampler;)V
+ public fun stopProfiler (Lio/sentry/ProfileLifecycle;)V
+}
+
+public class io/sentry/android/core/PerfettoProfiler {
+ public fun (Landroid/content/Context;Lio/sentry/ILogger;Lio/sentry/ISentryExecutorService;)V
+ public fun endAndCollect (Ljava/util/function/Consumer;)V
+ public fun start (J)Z
+}
+
public final class io/sentry/android/core/ScreenshotEventProcessor : io/sentry/EventProcessor {
- public fun (Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/BuildInfoProvider;)V
+ public fun (Lio/sentry/android/core/SentryAndroidOptions;Lio/sentry/android/core/BuildInfoProvider;Z)V
public fun getOrder ()Ljava/lang/Long;
public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent;
public fun process (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction;
@@ -309,69 +405,95 @@ public final class io/sentry/android/core/SentryAndroidDateProvider : io/sentry/
public final class io/sentry/android/core/SentryAndroidOptions : io/sentry/SentryOptions {
public fun ()V
public fun enableAllAutoBreadcrumbs (Z)V
+ public fun getAnrProfilingSampleRate ()Ljava/lang/Double;
public fun getAnrTimeoutIntervalMillis ()J
public fun getBeforeScreenshotCaptureCallback ()Lio/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback;
public fun getBeforeViewHierarchyCaptureCallback ()Lio/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback;
public fun getDebugImagesLoader ()Lio/sentry/android/core/IDebugImagesLoader;
public fun getFrameMetricsCollector ()Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;
public fun getNativeSdkName ()Ljava/lang/String;
+ public fun getNdkAppHangTimeoutIntervalMillis ()J
public fun getNdkHandlerStrategy ()I
+ public fun getScreenshot ()Lio/sentry/android/core/SentryScreenshotOptions;
public fun getStartupCrashDurationThresholdMillis ()J
public fun isAnrEnabled ()Z
+ public fun isAnrProfilingEnabled ()Z
public fun isAnrReportInDebug ()Z
public fun isAttachAnrThreadDump ()Z
+ public fun isAttachRawTombstone ()Z
public fun isAttachScreenshot ()Z
public fun isAttachViewHierarchy ()Z
public fun isCollectAdditionalContext ()Z
+ public fun isCollectExternalStorageContext ()Z
public fun isEnableActivityLifecycleBreadcrumbs ()Z
public fun isEnableActivityLifecycleTracingAutoFinish ()Z
+ public fun isEnableAnrFingerprinting ()Z
public fun isEnableAppComponentBreadcrumbs ()Z
public fun isEnableAppLifecycleBreadcrumbs ()Z
public fun isEnableAutoActivityLifecycleTracing ()Z
public fun isEnableAutoTraceIdGeneration ()Z
public fun isEnableFramesTracking ()Z
public fun isEnableNdk ()Z
+ public fun isEnableNdkAppHangTracking ()Z
public fun isEnableNetworkEventBreadcrumbs ()Z
public fun isEnablePerformanceV2 ()Z
public fun isEnableRootCheck ()Z
public fun isEnableScopeSync ()Z
+ public fun isEnableStandaloneAppStartTracing ()Z
public fun isEnableSystemEventBreadcrumbs ()Z
public fun isEnableSystemEventBreadcrumbsExtras ()Z
public fun isReportHistoricalAnrs ()Z
+ public fun isReportHistoricalTombstones ()Z
+ public fun isTombstoneEnabled ()Z
public fun setAnrEnabled (Z)V
+ public fun setAnrProfilingSampleRate (Ljava/lang/Double;)V
public fun setAnrReportInDebug (Z)V
public fun setAnrTimeoutIntervalMillis (J)V
public fun setAttachAnrThreadDump (Z)V
+ public fun setAttachRawTombstone (Z)V
public fun setAttachScreenshot (Z)V
public fun setAttachViewHierarchy (Z)V
public fun setBeforeScreenshotCaptureCallback (Lio/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback;)V
public fun setBeforeViewHierarchyCaptureCallback (Lio/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback;)V
public fun setCollectAdditionalContext (Z)V
+ public fun setCollectExternalStorageContext (Z)V
public fun setDebugImagesLoader (Lio/sentry/android/core/IDebugImagesLoader;)V
public fun setEnableActivityLifecycleBreadcrumbs (Z)V
public fun setEnableActivityLifecycleTracingAutoFinish (Z)V
+ public fun setEnableAnrFingerprinting (Z)V
public fun setEnableAppComponentBreadcrumbs (Z)V
public fun setEnableAppLifecycleBreadcrumbs (Z)V
public fun setEnableAutoActivityLifecycleTracing (Z)V
public fun setEnableAutoTraceIdGeneration (Z)V
public fun setEnableFramesTracking (Z)V
public fun setEnableNdk (Z)V
+ public fun setEnableNdkAppHangTracking (Z)V
public fun setEnableNetworkEventBreadcrumbs (Z)V
public fun setEnablePerformanceV2 (Z)V
public fun setEnableRootCheck (Z)V
public fun setEnableScopeSync (Z)V
+ public fun setEnableStandaloneAppStartTracing (Z)V
public fun setEnableSystemEventBreadcrumbs (Z)V
public fun setEnableSystemEventBreadcrumbsExtras (Z)V
public fun setFrameMetricsCollector (Lio/sentry/android/core/internal/util/SentryFrameMetricsCollector;)V
public fun setNativeHandlerStrategy (Lio/sentry/android/core/NdkHandlerStrategy;)V
public fun setNativeSdkName (Ljava/lang/String;)V
+ public fun setNdkAppHangTimeoutIntervalMillis (J)V
public fun setReportHistoricalAnrs (Z)V
+ public fun setReportHistoricalTombstones (Z)V
+ public fun setTombstoneEnabled (Z)V
}
public abstract interface class io/sentry/android/core/SentryAndroidOptions$BeforeCaptureCallback {
public abstract fun execute (Lio/sentry/SentryEvent;Lio/sentry/Hint;Z)Z
}
+public final class io/sentry/android/core/SentryFramesDelayResult {
+ public fun (DI)V
+ public fun getDelaySeconds ()D
+ public fun getFramesContributingToDelayCount ()I
+}
+
public final class io/sentry/android/core/SentryInitProvider {
public fun ()V
public fun attachInfo (Landroid/content/Context;Landroid/content/pm/ProviderInfo;)V
@@ -406,6 +528,25 @@ public final class io/sentry/android/core/SentryPerformanceProvider {
public fun shutdown ()V
}
+public final class io/sentry/android/core/SentryScreenshotOptions : io/sentry/SentryMaskingOptions {
+ public fun ()V
+ public fun setMaskAllImages (Z)V
+ public fun trackCustomMasking ()V
+}
+
+public final class io/sentry/android/core/SentryShakeDetector : android/hardware/SensorEventListener {
+ public fun (Lio/sentry/ILogger;)V
+ public fun close ()V
+ public fun onAccuracyChanged (Landroid/hardware/Sensor;I)V
+ public fun onSensorChanged (Landroid/hardware/SensorEvent;)V
+ public fun start (Landroid/content/Context;Lio/sentry/android/core/SentryShakeDetector$Listener;)V
+ public fun stop ()V
+}
+
+public abstract interface class io/sentry/android/core/SentryShakeDetector$Listener {
+ public abstract fun onShake ()V
+}
+
public class io/sentry/android/core/SentryUserFeedbackButton : android/widget/Button {
public fun (Landroid/content/Context;)V
public fun (Landroid/content/Context;Landroid/util/AttributeSet;)V
@@ -414,23 +555,46 @@ public class io/sentry/android/core/SentryUserFeedbackButton : android/widget/Bu
public fun setOnClickListener (Landroid/view/View$OnClickListener;)V
}
-public final class io/sentry/android/core/SentryUserFeedbackDialog : android/app/AlertDialog {
- public fun setCancelable (Z)V
- public fun setOnDismissListener (Landroid/content/DialogInterface$OnDismissListener;)V
- public fun show ()V
+public final class io/sentry/android/core/SentryUserFeedbackDialog : io/sentry/android/core/SentryUserFeedbackForm {
}
-public class io/sentry/android/core/SentryUserFeedbackDialog$Builder {
+public class io/sentry/android/core/SentryUserFeedbackDialog$Builder : io/sentry/android/core/SentryUserFeedbackForm$Builder {
public fun (Landroid/content/Context;)V
public fun (Landroid/content/Context;I)V
public fun (Landroid/content/Context;ILio/sentry/android/core/SentryUserFeedbackDialog$OptionsConfiguration;)V
public fun (Landroid/content/Context;Lio/sentry/android/core/SentryUserFeedbackDialog$OptionsConfiguration;)V
public fun associatedEventId (Lio/sentry/protocol/SentryId;)Lio/sentry/android/core/SentryUserFeedbackDialog$Builder;
+ public synthetic fun associatedEventId (Lio/sentry/protocol/SentryId;)Lio/sentry/android/core/SentryUserFeedbackForm$Builder;
public fun configurator (Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;)Lio/sentry/android/core/SentryUserFeedbackDialog$Builder;
+ public synthetic fun configurator (Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;)Lio/sentry/android/core/SentryUserFeedbackForm$Builder;
public fun create ()Lio/sentry/android/core/SentryUserFeedbackDialog;
+ public synthetic fun create ()Lio/sentry/android/core/SentryUserFeedbackForm;
+}
+
+public abstract interface class io/sentry/android/core/SentryUserFeedbackDialog$OptionsConfiguration : io/sentry/android/core/SentryUserFeedbackForm$OptionsConfiguration {
}
-public abstract interface class io/sentry/android/core/SentryUserFeedbackDialog$OptionsConfiguration {
+public class io/sentry/android/core/SentryUserFeedbackForm : android/app/AlertDialog {
+ protected fun onCreate (Landroid/os/Bundle;)V
+ public fun onDetachedFromWindow ()V
+ protected fun onStart ()V
+ protected fun onStop ()V
+ public fun setCancelable (Z)V
+ public fun setOnDismissListener (Landroid/content/DialogInterface$OnDismissListener;)V
+ public fun show ()V
+}
+
+public class io/sentry/android/core/SentryUserFeedbackForm$Builder {
+ public fun (Landroid/content/Context;)V
+ public fun (Landroid/content/Context;I)V
+ public fun (Landroid/content/Context;ILio/sentry/android/core/SentryUserFeedbackForm$OptionsConfiguration;)V
+ public fun (Landroid/content/Context;Lio/sentry/android/core/SentryUserFeedbackForm$OptionsConfiguration;)V
+ public fun associatedEventId (Lio/sentry/protocol/SentryId;)Lio/sentry/android/core/SentryUserFeedbackForm$Builder;
+ public fun configurator (Lio/sentry/SentryFeedbackOptions$OptionsConfigurator;)Lio/sentry/android/core/SentryUserFeedbackForm$Builder;
+ public fun create ()Lio/sentry/android/core/SentryUserFeedbackForm;
+}
+
+public abstract interface class io/sentry/android/core/SentryUserFeedbackForm$OptionsConfiguration {
public abstract fun configure (Landroid/content/Context;Lio/sentry/SentryFeedbackOptions;)V
}
@@ -445,6 +609,7 @@ public class io/sentry/android/core/SpanFrameMetricsCollector : io/sentry/IPerfo
public final class io/sentry/android/core/SystemEventsBreadcrumbsIntegration : io/sentry/Integration, io/sentry/android/core/AppState$AppStateListener, java/io/Closeable {
public fun (Landroid/content/Context;)V
+ public fun (Landroid/content/Context;Landroid/os/Handler;)V
public fun (Landroid/content/Context;Ljava/util/List;)V
public fun close ()V
public static fun getDefaultActions ()Ljava/util/List;
@@ -453,6 +618,29 @@ public final class io/sentry/android/core/SystemEventsBreadcrumbsIntegration : i
public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
}
+public class io/sentry/android/core/TombstoneIntegration : io/sentry/Integration, java/io/Closeable {
+ public fun (Landroid/content/Context;)V
+ public fun close ()V
+ public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
+}
+
+public final class io/sentry/android/core/TombstoneIntegration$TombstoneHint : io/sentry/hints/BlockingFlushHint, io/sentry/hints/Backfillable, io/sentry/hints/NativeCrashExit {
+ public fun (JLio/sentry/ILogger;JZ)V
+ public fun isFlushable (Lio/sentry/protocol/SentryId;)Z
+ public fun setFlushable (Lio/sentry/protocol/SentryId;)V
+ public fun shouldEnrich ()Z
+ public fun timestamp ()Ljava/lang/Long;
+}
+
+public class io/sentry/android/core/TombstoneIntegration$TombstonePolicy : io/sentry/android/core/ApplicationExitInfoHistoryDispatcher$ApplicationExitInfoPolicy {
+ public fun (Lio/sentry/android/core/SentryAndroidOptions;Landroid/content/Context;)V
+ public fun buildReport (Landroid/app/ApplicationExitInfo;Z)Lio/sentry/android/core/ApplicationExitInfoHistoryDispatcher$Report;
+ public fun getLabel ()Ljava/lang/String;
+ public fun getLastReportedTimestamp ()Ljava/lang/Long;
+ public fun getTargetReason ()I
+ public fun shouldReportHistorical ()Z
+}
+
public final class io/sentry/android/core/UserInteractionIntegration : android/app/Application$ActivityLifecycleCallbacks, io/sentry/Integration, java/io/Closeable {
public fun (Landroid/app/Application;Lio/sentry/util/LoadClass;)V
public fun close ()V
@@ -478,12 +666,89 @@ public final class io/sentry/android/core/ViewHierarchyEventProcessor : io/sentr
public static fun snapshotViewHierarchyAsData (Landroid/app/Activity;Lio/sentry/util/thread/IThreadChecker;Lio/sentry/ISerializer;Lio/sentry/ILogger;)[B
}
+public class io/sentry/android/core/anr/AggregatedStackTrace {
+ public fun ([Ljava/lang/StackTraceElement;IIJF)V
+ public fun addOccurrence (J)V
+ public fun getStack ()[Ljava/lang/StackTraceElement;
+}
+
+public class io/sentry/android/core/anr/AnrCulpritIdentifier {
+ public fun ()V
+ public static fun identify (Ljava/util/List;)Lio/sentry/android/core/anr/AggregatedStackTrace;
+ public static fun isSystemFrame (Ljava/lang/String;)Z
+}
+
+public class io/sentry/android/core/anr/AnrProfile {
+ public final field endTimeMs J
+ public final field stacks Ljava/util/List;
+ public final field startTimeMs J
+ public fun (Ljava/util/List;)V
+}
+
+public class io/sentry/android/core/anr/AnrProfileManager : java/lang/AutoCloseable {
+ public fun (Lio/sentry/SentryOptions;)V
+ public fun (Lio/sentry/SentryOptions;Ljava/io/File;)V
+ public fun add (Lio/sentry/android/core/anr/AnrStackTrace;)V
+ public fun clear ()V
+ public fun close ()V
+ public fun load ()Lio/sentry/android/core/anr/AnrProfile;
+}
+
+public class io/sentry/android/core/anr/AnrProfileRotationHelper {
+ public fun ()V
+ public static fun deleteLastFile (Ljava/io/File;)Z
+ public static fun getFileForRecording (Ljava/io/File;)Ljava/io/File;
+ public static fun getLastFile (Ljava/io/File;)Ljava/io/File;
+ public static fun rotate ()V
+}
+
+public class io/sentry/android/core/anr/AnrProfilingIntegration : io/sentry/Integration, io/sentry/android/core/AppState$AppStateListener, java/io/Closeable, java/lang/Runnable {
+ public static final field POLLING_INTERVAL_MS J
+ public static final field THRESHOLD_ANR_MS J
+ public fun ()V
+ protected fun checkMainThread (Ljava/lang/Thread;)V
+ public fun close ()V
+ protected fun getProfileManager ()Lio/sentry/android/core/anr/AnrProfileManager;
+ protected fun getState ()Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
+ public fun onBackground ()V
+ public fun onForeground ()V
+ public fun register (Lio/sentry/IScopes;Lio/sentry/SentryOptions;)V
+ public fun run ()V
+}
+
+protected final class io/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState : java/lang/Enum {
+ public static final field ANR_DETECTED Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
+ public static final field IDLE Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
+ public static final field SUSPICIOUS Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
+ public static fun valueOf (Ljava/lang/String;)Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
+ public static fun values ()[Lio/sentry/android/core/anr/AnrProfilingIntegration$MainThreadState;
+}
+
+public final class io/sentry/android/core/anr/AnrStackTrace : java/lang/Comparable {
+ public final field stack [Ljava/lang/StackTraceElement;
+ public final field timestampMs J
+ public fun (J[Ljava/lang/StackTraceElement;)V
+ public fun compareTo (Lio/sentry/android/core/anr/AnrStackTrace;)I
+ public synthetic fun compareTo (Ljava/lang/Object;)I
+ public static fun deserialize (Ljava/io/DataInputStream;)Lio/sentry/android/core/anr/AnrStackTrace;
+ public fun serialize (Ljava/io/DataOutputStream;)V
+}
+
+public final class io/sentry/android/core/anr/StackTraceConverter {
+ public fun ()V
+ public static fun convert (Lio/sentry/android/core/anr/AnrProfile;)Lio/sentry/protocol/profiling/SentryProfile;
+}
+
public final class io/sentry/android/core/cache/AndroidEnvelopeCache : io/sentry/cache/EnvelopeCache {
+ public static final field LAST_ANR_MARKER_LABEL Ljava/lang/String;
public static final field LAST_ANR_REPORT Ljava/lang/String;
+ public static final field LAST_TOMBSTONE_MARKER_LABEL Ljava/lang/String;
+ public static final field LAST_TOMBSTONE_REPORT Ljava/lang/String;
public fun (Lio/sentry/android/core/SentryAndroidOptions;)V
public fun getDirectory ()Ljava/io/File;
public static fun hasStartupCrashMarker (Lio/sentry/SentryOptions;)Z
public static fun lastReportedAnr (Lio/sentry/SentryOptions;)Ljava/lang/Long;
+ public static fun lastReportedTombstone (Lio/sentry/SentryOptions;)Ljava/lang/Long;
public fun store (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)V
public fun storeEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Z
}
@@ -525,14 +790,22 @@ public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/andr
public static final field staticLock Lio/sentry/util/AutoClosableReentrantLock;
public fun ()V
public fun addActivityLifecycleTimeSpans (Lio/sentry/android/core/performance/ActivityLifecycleTimeSpan;)V
+ public fun canExtendAppStart ()Z
public fun clear ()V
public fun createProcessInitSpan ()Lio/sentry/android/core/performance/TimeSpan;
public fun getActivityLifecycleTimeSpans ()Ljava/util/List;
+ public fun getAppStartBaggageHeader ()Ljava/lang/String;
public fun getAppStartContinuousProfiler ()Lio/sentry/IContinuousProfiler;
+ public fun getAppStartEndTime ()Lio/sentry/SentryDate;
+ public fun getAppStartExtension ()Lio/sentry/android/core/AppStartExtension;
public fun getAppStartProfiler ()Lio/sentry/ITransactionProfiler;
+ public fun getAppStartReason ()Ljava/lang/String;
public fun getAppStartSamplingDecision ()Lio/sentry/TracesSamplingDecision;
+ public fun getAppStartSentryTraceHeader ()Ljava/lang/String;
public fun getAppStartTimeSpan ()Lio/sentry/android/core/performance/TimeSpan;
+ public fun getAppStartTimeSpanForHeadless ()Lio/sentry/android/core/performance/TimeSpan;
public fun getAppStartTimeSpanWithFallback (Lio/sentry/android/core/SentryAndroidOptions;)Lio/sentry/android/core/performance/TimeSpan;
+ public fun getAppStartTraceId ()Lio/sentry/protocol/SentryId;
public fun getAppStartType ()Lio/sentry/android/core/performance/AppStartMetrics$AppStartType;
public fun getApplicationOnCreateTimeSpan ()Lio/sentry/android/core/performance/TimeSpan;
public fun getClassLoadedUptimeMs ()J
@@ -553,12 +826,19 @@ public class io/sentry/android/core/performance/AppStartMetrics : io/sentry/andr
public static fun onContentProviderPostCreate (Landroid/content/ContentProvider;)V
public fun registerLifecycleCallbacks (Landroid/app/Application;)V
public fun setAppLaunchedInForeground (Z)V
+ public fun setAppStartBaggageHeader (Ljava/lang/String;)V
public fun setAppStartContinuousProfiler (Lio/sentry/IContinuousProfiler;)V
+ public fun setAppStartEndTime (Lio/sentry/SentryDate;)V
public fun setAppStartProfiler (Lio/sentry/ITransactionProfiler;)V
public fun setAppStartSamplingDecision (Lio/sentry/TracesSamplingDecision;)V
+ public fun setAppStartSentryTraceHeader (Ljava/lang/String;)V
+ public fun setAppStartTraceId (Lio/sentry/protocol/SentryId;)V
public fun setAppStartType (Lio/sentry/android/core/performance/AppStartMetrics$AppStartType;)V
+ public fun setCachedStartInfo (Landroid/app/ApplicationStartInfo;)V
public fun setClassLoadedUptimeMs (J)V
+ public fun setHeadlessAppStartListener (Lio/sentry/android/core/performance/AppStartMetrics$HeadlessAppStartListener;)V
public fun shouldSendStartMeasurements ()Z
+ public fun shouldSendStartMeasurements (Z)Z
}
public final class io/sentry/android/core/performance/AppStartMetrics$AppStartType : java/lang/Enum {
@@ -569,6 +849,10 @@ public final class io/sentry/android/core/performance/AppStartMetrics$AppStartTy
public static fun values ()[Lio/sentry/android/core/performance/AppStartMetrics$AppStartType;
}
+public abstract interface class io/sentry/android/core/performance/AppStartMetrics$HeadlessAppStartListener {
+ public abstract fun onHeadlessAppStart ()V
+}
+
public class io/sentry/android/core/performance/TimeSpan : java/lang/Comparable {
public fun ()V
public fun compareTo (Lio/sentry/android/core/performance/TimeSpan;)I
diff --git a/sentry-android-core/build.gradle.kts b/sentry-android-core/build.gradle.kts
index 99d6b5115c8..0e3708a89bf 100644
--- a/sentry-android-core/build.gradle.kts
+++ b/sentry-android-core/build.gradle.kts
@@ -1,11 +1,11 @@
import net.ltgt.gradle.errorprone.errorprone
import org.jetbrains.kotlin.config.KotlinCompilerVersion
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8
plugins {
id("com.android.library")
alias(libs.plugins.kotlin.android)
- jacoco
- alias(libs.plugins.jacoco.android)
+ alias(libs.plugins.kotlin.compose)
alias(libs.plugins.errorprone)
alias(libs.plugins.gradle.versions)
}
@@ -34,13 +34,23 @@ android {
getByName("release") { consumerProguardFiles("proguard-rules.pro") }
}
- kotlin { compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 }
+ // AGP 9 only generates unit tests for the testBuildType. The debug variant is
+ // disabled, so unit tests must target release to run at all.
+ testBuildType = "release"
+
+ kotlin { compilerOptions.jvmTarget = JVM_1_8 }
testOptions {
animationsDisabled = true
unitTests.apply {
isReturnDefaultValues = true
isIncludeAndroidResources = true
+ // Robolectric loads the android-all jar into each test JVM, which needs more heap
+ // than the default.
+ all {
+ it.minHeapSize = "256m"
+ it.maxHeapSize = "2g"
+ }
}
}
@@ -69,6 +79,13 @@ tasks.withType().configureEach {
}
}
+// Snapshot PNGs are written by ScreenshotEventProcessorTest at runtime but must be declared as
+// outputs so Gradle's build cache restores them on cache hits (otherwise the CLI upload step
+// finds an empty directory).
+tasks
+ .matching { it.name == "testReleaseUnitTest" }
+ .configureEach { outputs.dir(layout.buildDirectory.dir("test-snapshots")) }
+
dependencies {
api(projects.sentry)
compileOnly(libs.jetbrains.annotations)
@@ -83,6 +100,7 @@ dependencies {
implementation(libs.androidx.lifecycle.common.java8)
implementation(libs.androidx.lifecycle.process)
implementation(libs.androidx.core)
+ implementation(libs.epitaph)
errorprone(libs.errorprone.core)
errorprone(libs.nopen.checker)
@@ -97,15 +115,22 @@ dependencies {
testImplementation(libs.androidx.test.ext.junit)
testImplementation(libs.androidx.test.runner)
testImplementation(libs.awaitility.kotlin)
+ testImplementation(libs.google.truth)
testImplementation(libs.mockito.kotlin)
testImplementation(libs.mockito.inline)
testImplementation(projects.sentryTestSupport)
+ testImplementation(projects.sentrySpotlight)
testImplementation(projects.sentryAndroidFragment)
testImplementation(projects.sentryAndroidTimber)
testImplementation(projects.sentryAndroidReplay)
testImplementation(projects.sentryCompose)
testImplementation(projects.sentryAndroidNdk)
- testRuntimeOnly(libs.androidx.compose.ui)
+
+ testImplementation(libs.androidx.activity.compose)
+ testImplementation(libs.androidx.compose.ui)
+ testImplementation(libs.androidx.compose.foundation)
+ testImplementation(libs.androidx.compose.foundation.layout)
+ testImplementation(libs.androidx.compose.material3)
testRuntimeOnly(libs.androidx.fragment.ktx)
testRuntimeOnly(libs.timber)
}
diff --git a/sentry-android-core/proguard-rules.pro b/sentry-android-core/proguard-rules.pro
index 5ebad5ac0c8..a66e472b07c 100644
--- a/sentry-android-core/proguard-rules.pro
+++ b/sentry-android-core/proguard-rules.pro
@@ -1,7 +1,6 @@
##---------------Begin: proguard configuration for android-core ----------
##---------------Begin: proguard configuration for androidx.core ----------
--keep class androidx.core.view.GestureDetectorCompat { (...); }
-keep class androidx.core.app.FrameMetricsAggregator { (...); }
-keep interface androidx.core.view.ScrollingView { *; }
##---------------End: proguard configuration for androidx.core ----------
@@ -30,6 +29,11 @@
# https://developer.android.com/studio/build/shrink-code#decode-stack-trace
-keepattributes LineNumberTable,SourceFile
+# Preserve distinct runtime identities for custom Throwables. R8 horizontal class merging can
+# otherwise merge unrelated exception classes, causing the runtime type and retraced frames to
+# disagree. Unused Throwables may still be removed, and retained Throwables may still be obfuscated.
+-keep,allowshrinking,allowobfuscation class * extends java.lang.Throwable
+
# Keep Classnames for integrations
-keepnames class * implements io.sentry.Integration
@@ -54,6 +58,7 @@
-keepnames class io.sentry.android.core.ApplicationNotResponding
+
##---------------End: proguard configuration for android-core ----------
##---------------Begin: proguard configuration for sentry-apollo-3 ----------
@@ -76,6 +81,10 @@
##---------------Begin: proguard configuration for sentry-android-replay ----------
-dontwarn io.sentry.android.replay.ReplayIntegration
-dontwarn io.sentry.android.replay.DefaultReplayBreadcrumbConverter
+-dontwarn io.sentry.android.replay.util.MaskRenderer
+-dontwarn io.sentry.android.replay.util.ViewsKt
+-dontwarn io.sentry.android.replay.viewhierarchy.ViewHierarchyNode$Companion
+-dontwarn io.sentry.android.replay.viewhierarchy.ViewHierarchyNode
-keepnames class io.sentry.android.replay.ReplayIntegration
##---------------End: proguard configuration for sentry-android-replay ----------
@@ -83,3 +92,8 @@
-dontwarn io.sentry.android.distribution.DistributionIntegration
-keepnames class io.sentry.android.distribution.DistributionIntegration
##---------------End: proguard configuration for sentry-android-distribution ----------
+
+##---------------Begin: proguard configuration for sentry-spotlight ----------
+-dontwarn io.sentry.spotlight.SpotlightIntegration
+-keepnames class io.sentry.spotlight.SpotlightIntegration
+##---------------End: proguard configuration for sentry-spotlight ----------
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java
index ade8fdd37c7..3895819fc94 100644
--- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityFramesTracker.java
@@ -10,6 +10,7 @@
import io.sentry.protocol.MeasurementValue;
import io.sentry.protocol.SentryId;
import io.sentry.util.AutoClosableReentrantLock;
+import io.sentry.util.LazyEvaluator;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
@@ -30,7 +31,7 @@
*/
public final class ActivityFramesTracker {
- private @Nullable FrameMetricsAggregator frameMetricsAggregator = null;
+ private @NotNull LazyEvaluator frameMetricsAggregator;
private @NotNull final SentryAndroidOptions options;
private final @NotNull Map>
@@ -41,17 +42,18 @@ public final class ActivityFramesTracker {
private final @NotNull MainLooperHandler handler;
protected @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock();
+ private final @NotNull LazyEvaluator androidXAvailable;
+
public ActivityFramesTracker(
final @NotNull io.sentry.util.LoadClass loadClass,
final @NotNull SentryAndroidOptions options,
final @NotNull MainLooperHandler handler) {
- final boolean androidXAvailable =
- loadClass.isClassAvailable("androidx.core.app.FrameMetricsAggregator", options.getLogger());
+ androidXAvailable =
+ loadClass.isClassAvailableLazy(
+ "androidx.core.app.FrameMetricsAggregator", options.getLogger());
+ frameMetricsAggregator = new LazyEvaluator<>(() -> new FrameMetricsAggregator());
- if (androidXAvailable) {
- frameMetricsAggregator = new FrameMetricsAggregator();
- }
this.options = options;
this.handler = handler;
}
@@ -67,15 +69,15 @@ public ActivityFramesTracker(
final @NotNull io.sentry.util.LoadClass loadClass,
final @NotNull SentryAndroidOptions options,
final @NotNull MainLooperHandler handler,
- final @Nullable FrameMetricsAggregator frameMetricsAggregator) {
+ final @NotNull FrameMetricsAggregator frameMetricsAggregator) {
this(loadClass, options, handler);
- this.frameMetricsAggregator = frameMetricsAggregator;
+ this.frameMetricsAggregator = new LazyEvaluator<>(() -> frameMetricsAggregator);
}
@VisibleForTesting
public boolean isFrameMetricsAggregatorAvailable() {
- return frameMetricsAggregator != null
+ return androidXAvailable.getValue()
&& options.isEnableFramesTracking()
&& !options.isEnablePerformanceV2();
}
@@ -87,7 +89,8 @@ public void addActivity(final @NotNull Activity activity) {
return;
}
- runSafelyOnUiThread(() -> frameMetricsAggregator.add(activity), "FrameMetricsAggregator.add");
+ runSafelyOnUiThread(
+ () -> frameMetricsAggregator.getValue().add(activity), "FrameMetricsAggregator.add");
snapshotFrameCountsAtStart(activity);
}
}
@@ -104,11 +107,11 @@ private void snapshotFrameCountsAtStart(final @NotNull Activity activity) {
return null;
}
- if (frameMetricsAggregator == null) {
+ if (!androidXAvailable.getValue()) {
return null;
}
- final @Nullable SparseIntArray[] framesRates = frameMetricsAggregator.getMetrics();
+ final @Nullable SparseIntArray[] framesRates = frameMetricsAggregator.getValue().getMetrics();
int totalFrames = 0;
int slowFrames = 0;
@@ -153,7 +156,7 @@ public void setMetrics(final @NotNull Activity activity, final @NotNull SentryId
// there was no
// Observers, See
// https://android.googlesource.com/platform/frameworks/base/+/140ff5ea8e2d99edc3fbe63a43239e459334c76b
- runSafelyOnUiThread(() -> frameMetricsAggregator.remove(activity), null);
+ runSafelyOnUiThread(() -> frameMetricsAggregator.getValue().remove(activity), null);
final @Nullable FrameCounts frameCounts = diffFrameCountsAtEnd(activity);
@@ -215,8 +218,9 @@ public void setMetrics(final @NotNull Activity activity, final @NotNull SentryId
public void stop() {
try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
if (isFrameMetricsAggregatorAvailable()) {
- runSafelyOnUiThread(() -> frameMetricsAggregator.stop(), "FrameMetricsAggregator.stop");
- frameMetricsAggregator.reset();
+ runSafelyOnUiThread(
+ () -> frameMetricsAggregator.getValue().stop(), "FrameMetricsAggregator.stop");
+ frameMetricsAggregator.getValue().reset();
}
activityMeasurements.clear();
}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java
index 9d748e5a27a..f416df6a988 100644
--- a/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/ActivityLifecycleIntegration.java
@@ -9,6 +9,8 @@
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
+import io.sentry.Baggage;
+import io.sentry.BaggageHeader;
import io.sentry.FullyDisplayedReporter;
import io.sentry.IScope;
import io.sentry.IScopes;
@@ -18,6 +20,7 @@
import io.sentry.Instrumenter;
import io.sentry.Integration;
import io.sentry.NoOpTransaction;
+import io.sentry.PropagationContext;
import io.sentry.SentryDate;
import io.sentry.SentryLevel;
import io.sentry.SentryNanotimeDate;
@@ -33,6 +36,7 @@
import io.sentry.android.core.performance.AppStartMetrics;
import io.sentry.android.core.performance.TimeSpan;
import io.sentry.protocol.MeasurementValue;
+import io.sentry.protocol.SentryId;
import io.sentry.protocol.TransactionNameSource;
import io.sentry.util.AutoClosableReentrantLock;
import io.sentry.util.Objects;
@@ -40,7 +44,7 @@
import java.io.Closeable;
import java.io.IOException;
import java.lang.ref.WeakReference;
-import java.util.Date;
+import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.Future;
@@ -55,12 +59,22 @@ public final class ActivityLifecycleIntegration
implements Integration, Closeable, Application.ActivityLifecycleCallbacks {
static final String UI_LOAD_OP = "ui.load";
+ static final String STANDALONE_APP_START_OP = "app.start";
+ private static final String STANDALONE_APP_START_NAME = "App Start";
static final String APP_START_WARM = "app.start.warm";
static final String APP_START_COLD = "app.start.cold";
static final String TTID_OP = "ui.load.initial_display";
static final String TTFD_OP = "ui.load.full_display";
+ static final String APP_START_EXTENDED_OP = "app.start.extended";
+ static final String APP_START_EXTENDED_DESC = "Extended App Start";
static final long TTFD_TIMEOUT_MILLIS = 25000;
+ // If a headless app start and the following activity's ui.load are more than this far apart, they
+ // are treated as unrelated and not connected into the same trace.
+ static final long APP_START_TO_UI_LOAD_CONTINUATION_MAX_GAP_NANOS = TimeUnit.MINUTES.toNanos(1);
private static final String TRACE_ORIGIN = "auto.ui.activity";
+ static final String APP_START_SCREEN_DATA = "app.vitals.start.screen";
+ static final String APP_START_REASON_DATA = "app.vitals.start.reason";
+ static final String APP_START_TRACE_ORIGIN = "auto.app.start";
private final @NotNull Application application;
private final @NotNull BuildInfoProvider buildInfoProvider;
@@ -77,11 +91,12 @@ public final class ActivityLifecycleIntegration
private @Nullable FullyDisplayedReporter fullyDisplayedReporter = null;
private @Nullable ISpan appStartSpan;
+ private @Nullable ITransaction appStartTransaction;
private final @NotNull WeakHashMap ttidSpanMap = new WeakHashMap<>();
private final @NotNull WeakHashMap ttfdSpanMap = new WeakHashMap<>();
private final @NotNull WeakHashMap activitySpanHelpers =
new WeakHashMap<>();
- private @NotNull SentryDate lastPausedTime = new SentryNanotimeDate(new Date(0), 0);
+ private @NotNull SentryDate lastPausedTime = new SentryNanotimeDate(0, 0);
private @Nullable Future> ttfdAutoCloseFuture = null;
// WeakHashMap isn't thread safe but ActivityLifecycleCallbacks is only called from the
@@ -124,6 +139,14 @@ public void register(final @NotNull IScopes scopes, final @NotNull SentryOptions
timeToFullDisplaySpanEnabled = this.options.isEnableTimeToFullDisplayTracing();
application.registerActivityLifecycleCallbacks(this);
+
+ if (performanceEnabled && this.options.isEnableStandaloneAppStartTracing()) {
+ final @NotNull AppStartMetrics metrics = AppStartMetrics.getInstance();
+ metrics.setHeadlessAppStartListener(this::onHeadlessAppStart);
+ metrics.getAppStartExtension().setExtendAppStartListener(this::onExtendAppStartRequested);
+ addIntegrationToSdkVersion("StandaloneAppStart");
+ }
+
this.options.getLogger().log(SentryLevel.DEBUG, "ActivityLifecycleIntegration installed.");
addIntegrationToSdkVersion("ActivityLifecycle");
}
@@ -135,6 +158,9 @@ private boolean isPerformanceEnabled(final @NotNull SentryAndroidOptions options
@Override
public void close() throws IOException {
application.unregisterActivityLifecycleCallbacks(this);
+ final @NotNull AppStartMetrics metrics = AppStartMetrics.getInstance();
+ metrics.setHeadlessAppStartListener(null);
+ metrics.getAppStartExtension().setExtendAppStartListener(null);
if (options != null) {
options.getLogger().log(SentryLevel.DEBUG, "ActivityLifecycleIntegration removed.");
@@ -239,33 +265,111 @@ private void startTracing(final @NotNull Activity activity) {
transactionOptions.setAppStartTransaction(appStartSamplingDecision != null);
setSpanOrigin(transactionOptions);
- // we can only bind to the scope if there's no running transaction
- ITransaction transaction =
- scopes.startTransaction(
- new TransactionContext(
- activityName,
- TransactionNameSource.COMPONENT,
- UI_LOAD_OP,
- appStartSamplingDecision),
- transactionOptions);
+ // Guards the headless-start check below with !isExtensionActive so the eager extension's
+ // stored trace id isn't mistaken for a finished headless start.
+ final boolean isExtensionActive =
+ AppStartMetrics.getInstance().getAppStartExtension().isActive();
+
+ final @Nullable SentryId storedAppStartTraceId =
+ AppStartMetrics.getInstance().getAppStartTraceId();
+ final boolean isFollowingHeadlessAppStart =
+ !isExtensionActive && (storedAppStartTraceId != null);
+
+ final boolean isAppStart =
+ !(firstActivityCreated || appStartTime == null || coldStart == null);
+ final boolean createStandaloneAppStart =
+ isAppStart
+ && options.isEnableStandaloneAppStartTracing()
+ && !isFollowingHeadlessAppStart
+ && !isExtensionActive;
+
+ if (createStandaloneAppStart) {
+ final TransactionOptions appStartTransactionOptions = new TransactionOptions();
+ appStartTransactionOptions.setBindToScope(false);
+ appStartTransactionOptions.setStartTimestamp(appStartTime);
+ appStartTransactionOptions.setAppStartTransaction(appStartSamplingDecision != null);
+ appStartTransactionOptions.setOrigin(APP_START_TRACE_ORIGIN);
+
+ appStartTransaction =
+ scopes.startTransaction(
+ new TransactionContext(
+ STANDALONE_APP_START_NAME,
+ TransactionNameSource.COMPONENT,
+ STANDALONE_APP_START_OP,
+ appStartSamplingDecision),
+ appStartTransactionOptions);
+ appStartTransaction.setData(APP_START_SCREEN_DATA, activityName);
+ final @Nullable String appStartReason = AppStartMetrics.getInstance().getAppStartReason();
+ if (appStartReason != null) {
+ appStartTransaction.setData(APP_START_REASON_DATA, appStartReason);
+ }
+ }
+
+ // Continue either the foreground app.start above or an earlier headless app.start.
+ final @Nullable String continueSentryTrace;
+ final @Nullable String continueBaggage;
+ if (createStandaloneAppStart) {
+ continueSentryTrace = appStartTransaction.toSentryTrace().getValue();
+ final @Nullable BaggageHeader baggageHeader = appStartTransaction.toBaggageHeader(null);
+ continueBaggage = baggageHeader == null ? null : baggageHeader.getValue();
+ } else if (isExtensionActive
+ || (isFollowingHeadlessAppStart && isWithinAppStartContinuationWindow(ttidStartTime))) {
+ continueSentryTrace = AppStartMetrics.getInstance().getAppStartSentryTraceHeader();
+ continueBaggage = AppStartMetrics.getInstance().getAppStartBaggageHeader();
+ } else {
+ continueSentryTrace = null;
+ continueBaggage = null;
+ }
+
+ if (isExtensionActive && isAppStart) {
+ // Only the launch activity sets the screen, so a later activity can't overwrite it. A
+ // screen also keeps the processor from classifying the eager app.start as headless.
+ AppStartMetrics.getInstance()
+ .getAppStartExtension()
+ .setData(APP_START_SCREEN_DATA, activityName);
+ }
+
+ final @Nullable TransactionContext continuedContext =
+ continueSentryTrace == null
+ ? null
+ : continueUiLoadTrace(continueSentryTrace, continueBaggage, activityName);
+
+ final ITransaction transaction;
+ if (continuedContext != null) {
+ transaction = scopes.startTransaction(continuedContext, transactionOptions);
+ } else {
+ transaction =
+ scopes.startTransaction(
+ new TransactionContext(
+ activityName,
+ TransactionNameSource.COMPONENT,
+ UI_LOAD_OP,
+ appStartSamplingDecision),
+ transactionOptions);
+ }
+
+ if (isFollowingHeadlessAppStart || isExtensionActive) {
+ // Consume the stored app-start trace so a later activity doesn't reuse it.
+ AppStartMetrics.getInstance().setAppStartTraceId(null);
+ AppStartMetrics.getInstance().setAppStartSentryTraceHeader(null);
+ AppStartMetrics.getInstance().setAppStartBaggageHeader(null);
+ }
final SpanOptions spanOptions = new SpanOptions();
setSpanOrigin(spanOptions);
- // in case appStartTime isn't available, we don't create a span for it.
- if (!(firstActivityCreated || appStartTime == null || coldStart == null)) {
- // start specific span for app start
- appStartSpan =
- transaction.startChild(
- getAppStartOp(coldStart),
- getAppStartDesc(coldStart),
- appStartTime,
- Instrumenter.SENTRY,
- spanOptions);
-
- // in case there's already an end time (e.g. due to deferred SDK init)
- // we can finish the app-start span
- finishAppStartSpan();
+ if (isAppStart) {
+ if (!createStandaloneAppStart && !options.isEnableStandaloneAppStartTracing()) {
+ appStartSpan =
+ transaction.startChild(
+ getAppStartOp(coldStart),
+ getAppStartDesc(coldStart),
+ appStartTime,
+ Instrumenter.SENTRY,
+ spanOptions);
+
+ finishAppStartSpan();
+ }
}
final @NotNull ISpan ttidSpan =
transaction.startChild(
@@ -316,6 +420,61 @@ private void setSpanOrigin(final @NotNull SpanOptions spanOptions) {
spanOptions.setOrigin(TRACE_ORIGIN);
}
+ /**
+ * Whether the ui.load starting at {@code uiLoadStartTime} is close enough in time to the headless
+ * app start to belong to the same trace. If they are more than {@link
+ * #APP_START_TO_UI_LOAD_CONTINUATION_MAX_GAP_NANOS} apart, they are treated as unrelated. When
+ * the headless end time is unknown, we keep the previous behaviour and continue the trace.
+ */
+ private boolean isWithinAppStartContinuationWindow(final @NotNull SentryDate uiLoadStartTime) {
+ final @Nullable SentryDate appStartEndTime = AppStartMetrics.getInstance().getAppStartEndTime();
+ if (appStartEndTime == null) {
+ return true;
+ }
+ return uiLoadStartTime.diff(appStartEndTime) <= APP_START_TO_UI_LOAD_CONTINUATION_MAX_GAP_NANOS;
+ }
+
+ /**
+ * Builds a {@link TransactionContext} for the ui.load transaction that shares the standalone
+ * app.start trace (same traceId and sampleRand) while staying a sibling (no parentSpanId), rather
+ * than a child. The continued baggage keeps sampling decisions on the same sampleRand. Returns
+ * null if the trace cannot be continued, so callers can fall back.
+ */
+ private @Nullable TransactionContext continueUiLoadTrace(
+ final @NotNull String sentryTrace,
+ final @Nullable String baggage,
+ final @NotNull String activityName) {
+ if (options == null || !options.isTracingEnabled()) {
+ return null;
+ }
+ final @NotNull PropagationContext propagationContext =
+ PropagationContext.fromHeaders(
+ options.getLogger(),
+ sentryTrace,
+ baggage == null ? null : Collections.singletonList(baggage),
+ options);
+ final @Nullable Boolean parentSampled = propagationContext.isSampled();
+ final @NotNull Baggage continuedBaggage = propagationContext.getBaggage();
+ final @Nullable TracesSamplingDecision parentSamplingDecision =
+ parentSampled == null
+ ? null
+ : new TracesSamplingDecision(
+ parentSampled,
+ continuedBaggage.getSampleRate(),
+ propagationContext.getSampleRand());
+ final @NotNull TransactionContext context =
+ new TransactionContext(
+ propagationContext.getTraceId(),
+ propagationContext.getSpanId(),
+ null,
+ parentSamplingDecision,
+ continuedBaggage);
+ context.setName(activityName);
+ context.setTransactionNameSource(TransactionNameSource.COMPONENT);
+ context.setOperation(UI_LOAD_OP);
+ return context;
+ }
+
@VisibleForTesting
void applyScope(final @NotNull IScope scope, final @NotNull ITransaction transaction) {
scope.withTransaction(
@@ -440,8 +599,7 @@ public void onActivityPostCreated(
final @NotNull Activity activity, final @Nullable Bundle savedInstanceState) {
final ActivityLifecycleSpanHelper helper = activitySpanHelpers.get(activity);
if (helper != null) {
- helper.createAndStopOnCreateSpan(
- appStartSpan != null ? appStartSpan : activitiesWithOngoingTransactions.get(activity));
+ helper.createAndStopOnCreateSpan(getAppStartParent(activity));
}
}
@@ -479,11 +637,11 @@ public void onActivityStarted(final @NotNull Activity activity) {
public void onActivityPostStarted(final @NotNull Activity activity) {
final ActivityLifecycleSpanHelper helper = activitySpanHelpers.get(activity);
if (helper != null) {
- helper.createAndStopOnStartSpan(
- appStartSpan != null ? appStartSpan : activitiesWithOngoingTransactions.get(activity));
+ helper.createAndStopOnStartSpan(getAppStartParent(activity));
// Needed to handle hybrid SDKs
helper.saveSpanToAppStartMetrics();
}
+ finishAppStartSpan();
}
@Override
@@ -559,6 +717,9 @@ public void onActivityDestroyed(final @NotNull Activity activity) {
// in case the appStartSpan isn't completed yet, we finish it as cancelled to avoid
// memory leak
finishSpan(appStartSpan, SpanStatus.CANCELLED);
+ if (appStartTransaction != null && !appStartTransaction.isFinished()) {
+ appStartTransaction.finish(SpanStatus.CANCELLED);
+ }
// we finish the ttidSpan as cancelled in case it isn't completed yet
final ISpan ttidSpan = ttidSpanMap.get(activity);
@@ -575,6 +736,7 @@ public void onActivityDestroyed(final @NotNull Activity activity) {
// set it to null in case its been just finished as cancelled
appStartSpan = null;
+ appStartTransaction = null;
ttidSpanMap.remove(activity);
ttfdSpanMap.remove(activity);
}
@@ -592,7 +754,7 @@ public void onActivityDestroyed(final @NotNull Activity activity) {
private void clear() {
firstActivityCreated = false;
- lastPausedTime = new SentryNanotimeDate(new Date(0), 0);
+ lastPausedTime = new SentryNanotimeDate(0, 0);
activitySpanHelpers.clear();
}
@@ -637,22 +799,23 @@ private void onFirstFrameDrawn(final @Nullable ISpan ttfdSpan, final @Nullable I
final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance();
final @NotNull TimeSpan appStartTimeSpan = appStartMetrics.getAppStartTimeSpan();
final @NotNull TimeSpan sdkInitTimeSpan = appStartMetrics.getSdkInitTimeSpan();
+ final @Nullable SentryDate firstFrameEndDate =
+ options != null ? options.getDateProvider().now() : null;
// and we need to set the end time of the app start here, after the first frame is drawn.
if (appStartTimeSpan.hasStarted() && appStartTimeSpan.hasNotStopped()) {
- appStartTimeSpan.stop();
+ stopTimeSpanAtDate(appStartTimeSpan, firstFrameEndDate);
}
if (sdkInitTimeSpan.hasStarted() && sdkInitTimeSpan.hasNotStopped()) {
- sdkInitTimeSpan.stop();
+ stopTimeSpanAtDate(sdkInitTimeSpan, firstFrameEndDate);
}
- finishAppStartSpan();
+ finishAppStartSpan(firstFrameEndDate);
// Sentry.reportFullyDisplayed can be run in any thread, so we have to ensure synchronization
// with first frame drawn
try (final @NotNull ISentryLifecycleToken ignored = fullyDisplayedLock.acquire()) {
- if (options != null && ttidSpan != null) {
- final SentryDate endDate = options.getDateProvider().now();
- final long durationNanos = endDate.diff(ttidSpan.getStartDate());
+ if (options != null && ttidSpan != null && firstFrameEndDate != null) {
+ final long durationNanos = firstFrameEndDate.diff(ttidSpan.getStartDate());
final long durationMillis = TimeUnit.NANOSECONDS.toMillis(durationNanos);
ttidSpan.setMeasurement(
MeasurementValue.KEY_TIME_TO_INITIAL_DISPLAY, durationMillis, MILLISECOND);
@@ -664,10 +827,10 @@ private void onFirstFrameDrawn(final @Nullable ISpan ttfdSpan, final @Nullable I
MeasurementValue.KEY_TIME_TO_FULL_DISPLAY, durationMillis, MILLISECOND);
ttfdSpan.setMeasurement(
MeasurementValue.KEY_TIME_TO_FULL_DISPLAY, durationMillis, MILLISECOND);
- finishSpan(ttfdSpan, endDate);
+ finishSpan(ttfdSpan, firstFrameEndDate);
}
- finishSpan(ttidSpan, endDate);
+ finishSpan(ttidSpan, firstFrameEndDate);
} else {
finishSpan(ttidSpan);
if (fullyDisplayedCalled) {
@@ -677,6 +840,17 @@ private void onFirstFrameDrawn(final @Nullable ISpan ttfdSpan, final @Nullable I
}
}
+ private void stopTimeSpanAtDate(
+ final @NotNull TimeSpan timeSpan, final @Nullable SentryDate endDate) {
+ final @Nullable SentryDate startDate = timeSpan.getStartTimestamp();
+ if (endDate != null && startDate != null) {
+ final long durationMillis = TimeUnit.NANOSECONDS.toMillis(endDate.diff(startDate));
+ timeSpan.setStoppedAt(timeSpan.getStartUptimeMs() + durationMillis);
+ } else {
+ timeSpan.stop();
+ }
+ }
+
private void onFullFrameDrawn(final @NotNull ISpan ttidSpan, final @NotNull ISpan ttfdSpan) {
cancelTtfdAutoClose();
// Sentry.reportFullyDisplayed can be run in any thread, so we have to ensure synchronization
@@ -779,6 +953,16 @@ WeakHashMap getTtfdSpanMap() {
}
}
+ private @Nullable ISpan getAppStartParent(final @NotNull Activity activity) {
+ if (appStartTransaction != null) {
+ return appStartTransaction;
+ }
+ if (appStartSpan != null) {
+ return appStartSpan;
+ }
+ return activitiesWithOngoingTransactions.get(activity);
+ }
+
private @NotNull String getAppStartOp(final boolean coldStart) {
if (coldStart) {
return APP_START_COLD;
@@ -788,12 +972,166 @@ WeakHashMap getTtfdSpanMap() {
}
private void finishAppStartSpan() {
+ finishAppStartSpan(null);
+ }
+
+ private void finishAppStartSpan(final @Nullable SentryDate endDate) {
final @Nullable SentryDate appStartEndTime =
- AppStartMetrics.getInstance()
- .getAppStartTimeSpanWithFallback(options)
- .getProjectedStopTimestamp();
+ endDate != null
+ ? endDate
+ : AppStartMetrics.getInstance()
+ .getAppStartTimeSpanWithFallback(options)
+ .getProjectedStopTimestamp();
if (performanceEnabled && appStartEndTime != null) {
finishSpan(appStartSpan, appStartEndTime);
+ if (appStartTransaction != null && !appStartTransaction.isFinished()) {
+ appStartTransaction.finish(SpanStatus.OK, appStartEndTime);
+ }
+ // Finish the eager extended transaction at the natural first-frame end. waitForChildren keeps
+ // it open until the extended span finishes; no-op if the app start was not extended.
+ AppStartMetrics.getInstance().getAppStartExtension().finishTransaction(appStartEndTime);
+ }
+ }
+
+ private void onHeadlessAppStart() {
+ if (scopes == null || options == null || !performanceEnabled) {
+ return;
+ }
+
+ final @NotNull AppStartMetrics metrics = AppStartMetrics.getInstance();
+ // Profilers are stopped for headless starts; clear the decision so it doesn't
+ // leak to a later ui.load transaction if an activity eventually opens.
+ metrics.setAppStartSamplingDecision(null);
+
+ // For headless starts, appLaunchedInForeground is false, so we can't use
+ // getAppStartTimeSpanWithFallback (which gates on foreground).
+ final @NotNull TimeSpan appStartTimeSpan = metrics.getAppStartTimeSpanForHeadless();
+
+ if (!appStartTimeSpan.hasStarted() || !appStartTimeSpan.hasStopped()) {
+ return;
+ }
+
+ final @Nullable SentryDate startTime = appStartTimeSpan.getStartTimestamp();
+ final @Nullable SentryDate endTime = appStartTimeSpan.getProjectedStopTimestamp();
+ if (startTime == null || endTime == null) {
+ return;
+ }
+
+ // Persist the end time so a later ui.load can tell whether it is close enough to continue this
+ // trace; without it the continuation window is unbounded.
+ metrics.setAppStartEndTime(endTime);
+
+ final @NotNull AppStartExtension extension = metrics.getAppStartExtension();
+ if (extension.isActive()) {
+ extension.finishTransaction(endTime);
+ return;
+ }
+ if (!metrics.shouldSendStartMeasurements(true)) {
+ return;
+ }
+
+ final @NotNull ITransaction transaction =
+ createStandaloneAppStartTransaction(startTime, null, false);
+ transaction.finish(SpanStatus.OK, endTime);
+ }
+
+ /**
+ * Creates the standalone {@code app.start} transaction (not bound to the scope) and persists its
+ * trace headers so a later {@code ui.load} can share the same trace. Shared by the headless path
+ * and the eager extension path. When {@code holdOpenForExtension} is true, the transaction waits
+ * for its children and gets a deadline so it stays open until the extended span finishes.
+ */
+ private @NotNull ITransaction createStandaloneAppStartTransaction(
+ final @NotNull SentryDate startTime,
+ final @Nullable TracesSamplingDecision samplingDecision,
+ final boolean holdOpenForExtension) {
+ final @NotNull AppStartMetrics metrics = AppStartMetrics.getInstance();
+
+ final TransactionOptions txnOptions = new TransactionOptions();
+ txnOptions.setBindToScope(false);
+ txnOptions.setStartTimestamp(startTime);
+ txnOptions.setOrigin(APP_START_TRACE_ORIGIN);
+ txnOptions.setAppStartTransaction(samplingDecision != null);
+ if (holdOpenForExtension) {
+ txnOptions.setWaitForChildren(true);
+ final long deadlineTimeoutMillis = options.getDeadlineTimeout();
+ txnOptions.setDeadlineTimeout(deadlineTimeoutMillis <= 0 ? null : deadlineTimeoutMillis);
+ // Persist the end time (covering every finish path: user finish, first frame, deadline) so a
+ // later ui.load can tell whether it is close enough to continue this trace; without it the
+ // continuation window is unbounded.
+ txnOptions.setTransactionFinishedCallback(
+ finishedTransaction ->
+ AppStartMetrics.getInstance()
+ .setAppStartEndTime(finishedTransaction.getFinishDate()));
}
+
+ final @NotNull TransactionContext txnContext =
+ new TransactionContext(
+ STANDALONE_APP_START_NAME,
+ TransactionNameSource.COMPONENT,
+ STANDALONE_APP_START_OP,
+ samplingDecision);
+
+ final @NotNull ITransaction transaction = scopes.startTransaction(txnContext, txnOptions);
+ final @Nullable String appStartReason = metrics.getAppStartReason();
+ if (appStartReason != null) {
+ transaction.setData(APP_START_REASON_DATA, appStartReason);
+ }
+ metrics.setAppStartTraceId(transaction.getSpanContext().getTraceId());
+ // Persist trace headers so a later ui.load can share traceId and sampleRand.
+ metrics.setAppStartSentryTraceHeader(transaction.toSentryTrace().getValue());
+ final @Nullable BaggageHeader baggageHeader = transaction.toBaggageHeader(null);
+ metrics.setAppStartBaggageHeader(baggageHeader == null ? null : baggageHeader.getValue());
+ return transaction;
+ }
+
+ /**
+ * Handles {@code Sentry.extendAppStart()}: eagerly creates the standalone app.start transaction
+ * and the extended child span (we have scopes here), then hands both to the {@link
+ * AppStartExtension}, which owns them. The transaction is held open ({@code waitForChildren})
+ * until the user calls {@code Sentry.finishExtendedAppStart()} or the deadline forces it.
+ * Standalone-only: this is only registered as a listener when standalone app start tracing is
+ * enabled.
+ */
+ private @Nullable AppStartExtension.ExtendedAppStart onExtendAppStartRequested() {
+ if (scopes == null
+ || options == null
+ || !performanceEnabled
+ || !options.isEnableStandaloneAppStartTracing()) {
+ return null;
+ }
+ final @NotNull AppStartMetrics metrics = AppStartMetrics.getInstance();
+
+ final @NotNull TimeSpan appStartTimeSpan =
+ metrics.getAppStartTimeSpan().hasStarted()
+ ? metrics.getAppStartTimeSpan()
+ : metrics.getSdkInitTimeSpan();
+ final @Nullable SentryDate startTime = appStartTimeSpan.getStartTimestamp();
+ if (startTime == null) {
+ return null;
+ }
+
+ // The app start sampling decision was pre-rolled on the previous run so the app start
+ // profiler could start before Sentry.init. It forces the trace sampling of the eager
+ // app.start transaction created below (no re-roll, staying consistent with whether the
+ // profiler actually started) and lets it bind the app start profiler. It's single-use:
+ // we clear it so the first ui.load can't also claim it.
+ final @Nullable TracesSamplingDecision samplingDecision = metrics.getAppStartSamplingDecision();
+ metrics.setAppStartSamplingDecision(null);
+
+ final @NotNull ITransaction transaction =
+ createStandaloneAppStartTransaction(startTime, samplingDecision, true);
+
+ final SpanOptions spanOptions = new SpanOptions();
+ setSpanOrigin(spanOptions);
+ final @NotNull ISpan extendedSpan =
+ transaction.startChild(
+ APP_START_EXTENDED_OP,
+ APP_START_EXTENDED_DESC,
+ AndroidDateUtils.getCurrentSentryDateTime(),
+ Instrumenter.SENTRY,
+ spanOptions);
+
+ return new AppStartExtension.ExtendedAppStart(transaction, extendedSpan);
}
}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java
index a3fb6f6c8db..a1c0c097cb9 100644
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidContinuousProfiler.java
@@ -26,6 +26,7 @@
import io.sentry.protocol.SentryId;
import io.sentry.transport.RateLimiter;
import io.sentry.util.AutoClosableReentrantLock;
+import io.sentry.util.LazyEvaluator;
import io.sentry.util.SentryRandom;
import java.util.ArrayList;
import java.util.List;
@@ -37,6 +38,11 @@
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.VisibleForTesting;
+/**
+ * Legacy Android implementation of {@link IContinuousProfiler}, using Android's {@code
+ * Debug.startMethodTracingSampling} See {@link PerfettoContinuousProfiler} for the new
+ * implementation using {@code ProfilingManager}, available on API 35+.
+ */
@ApiStatus.Internal
public class AndroidContinuousProfiler
implements IContinuousProfiler, RateLimiter.IRateLimitObserver {
@@ -45,7 +51,7 @@ public class AndroidContinuousProfiler
private final @NotNull ILogger logger;
private final @Nullable String profilingTracesDirPath;
private final int profilingTracesHz;
- private final @NotNull ISentryExecutorService executorService;
+ private final @NotNull LazyEvaluator.Evaluator executorServiceSupplier;
private final @NotNull BuildInfoProvider buildInfoProvider;
private boolean isInitialized = false;
private final @NotNull SentryFrameMetricsCollector frameMetricsCollector;
@@ -73,13 +79,13 @@ public AndroidContinuousProfiler(
final @NotNull ILogger logger,
final @Nullable String profilingTracesDirPath,
final int profilingTracesHz,
- final @NotNull ISentryExecutorService executorService) {
+ final @NotNull LazyEvaluator.Evaluator executorServiceSupplier) {
this.logger = logger;
this.frameMetricsCollector = frameMetricsCollector;
this.buildInfoProvider = buildInfoProvider;
this.profilingTracesDirPath = profilingTracesDirPath;
this.profilingTracesHz = profilingTracesHz;
- this.executorService = executorService;
+ this.executorServiceSupplier = executorServiceSupplier;
}
private void init() {
@@ -190,6 +196,7 @@ private void start() {
}
// If device is offline, we don't start the profiler, to avoid flooding the cache
+ // TODO .getConnectionStatus() may be blocking, investigate if this can be done async
if (scopes.getOptions().getConnectionStatusProvider().getConnectionStatus() == DISCONNECTED) {
logger.log(SentryLevel.WARNING, "Device is offline. Stopping profiler.");
// Let's stop and reset profiler id, as the profile is now broken anyway
@@ -208,11 +215,11 @@ private void start() {
isRunning = true;
- if (profilerId == SentryId.EMPTY_ID) {
+ if (profilerId.equals(SentryId.EMPTY_ID)) {
profilerId = new SentryId();
}
- if (chunkId == SentryId.EMPTY_ID) {
+ if (chunkId.equals(SentryId.EMPTY_ID)) {
chunkId = new SentryId();
}
@@ -221,7 +228,8 @@ private void start() {
}
try {
- stopFuture = executorService.schedule(() -> stop(true), MAX_CHUNK_DURATION_MILLIS);
+ stopFuture =
+ executorServiceSupplier.evaluate().schedule(() -> stop(true), MAX_CHUNK_DURATION_MILLIS);
} catch (RejectedExecutionException e) {
logger.log(
SentryLevel.ERROR,
@@ -300,7 +308,8 @@ private void stop(final boolean restartProfiler) {
chunkId,
endData.measurementsMap,
endData.traceFile,
- startProfileChunkTimestamp));
+ startProfileChunkTimestamp,
+ ProfileChunk.PLATFORM_ANDROID));
}
}
@@ -344,6 +353,11 @@ public void close(final boolean isTerminating) {
return profilerId;
}
+ @Override
+ public @NotNull SentryId getChunkId() {
+ return chunkId;
+ }
+
private void sendChunks(final @NotNull IScopes scopes, final @NotNull SentryOptions options) {
try {
options
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidCpuCollector.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidCpuCollector.java
index ea7a20deab1..cb8e148b318 100644
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidCpuCollector.java
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidCpuCollector.java
@@ -1,56 +1,42 @@
package io.sentry.android.core;
+import android.os.Process;
import android.os.SystemClock;
import android.system.Os;
import android.system.OsConstants;
import io.sentry.ILogger;
import io.sentry.IPerformanceSnapshotCollector;
import io.sentry.PerformanceCollectionData;
-import io.sentry.SentryLevel;
-import io.sentry.util.FileUtils;
import io.sentry.util.Objects;
-import java.io.File;
-import java.io.IOException;
-import java.util.regex.Pattern;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
-// The approach to get the cpu usage info was taken from
-// https://eng.lyft.com/monitoring-cpu-performance-of-lyfts-android-applications-4e36fafffe12
-// The content of the /proc/self/stat file is specified in
-// https://man7.org/linux/man-pages/man5/proc.5.html
+// The process cpu time comes from Process.getElapsedCpuTime(), a @CriticalNative wrapper around
+// clock_gettime(CLOCK_PROCESS_CPUTIME_ID), rather than from parsing /proc/self/stat: reading and
+// parsing that file allocated on every sample, and collect() runs 10 times per second for the whole
+// duration of a transaction. It does not include the cpu time of reaped child processes, which an
+// app process doesn't have.
@ApiStatus.Internal
public final class AndroidCpuCollector implements IPerformanceSnapshotCollector {
+ private static final long NANOSECONDS_PER_MILLISECOND = 1_000_000;
+
private long lastRealtimeNanos = 0;
private long lastCpuNanos = 0;
- /** Number of clock ticks per second. */
- private long clockSpeedHz = 1;
-
private long numCores = 1;
- private final long NANOSECOND_PER_SECOND = 1_000_000_000;
-
- /** Number of nanoseconds per clock tick. */
- private double nanosecondsPerClockTick = NANOSECOND_PER_SECOND / (double) clockSpeedHz;
- /** File containing stats about this process. */
- private final @NotNull File selfStat = new File("/proc/self/stat");
-
- private final @NotNull ILogger logger;
private boolean isEnabled = false;
- private final @NotNull Pattern newLinePattern = Pattern.compile("[\n\t\r ]");
public AndroidCpuCollector(final @NotNull ILogger logger) {
- this.logger = Objects.requireNonNull(logger, "Logger is required.");
+ Objects.requireNonNull(logger, "Logger is required.");
}
@Override
public void setup() {
isEnabled = true;
- clockSpeedHz = Os.sysconf(OsConstants._SC_CLK_TCK);
numCores = Os.sysconf(OsConstants._SC_NPROCESSORS_CONF);
- nanosecondsPerClockTick = NANOSECOND_PER_SECOND / (double) clockSpeedHz;
+ lastRealtimeNanos = SystemClock.elapsedRealtimeNanos();
lastCpuNanos = readTotalCpuNanos();
}
@@ -74,36 +60,7 @@ public void collect(final @NotNull PerformanceCollectionData performanceCollecti
(cpuUsagePercentage / (double) numCores) * 100.0);
}
- /** Read the /proc/self/stat file and parses the result. */
private long readTotalCpuNanos() {
- String stat = null;
- try {
- stat = FileUtils.readText(selfStat);
- } catch (IOException e) {
- // If an error occurs when reading the file, we avoid reading it again until the setup method
- // is called again
- isEnabled = false;
- logger.log(
- SentryLevel.WARNING, "Unable to read /proc/self/stat file. Disabling cpu collection.", e);
- }
- if (stat != null) {
- stat = stat.trim();
- String[] stats = newLinePattern.split(stat);
- try {
- // Amount of clock ticks this process has been scheduled in user mode
- long uTime = Long.parseLong(stats[13]);
- // Amount of clock ticks this process has been scheduled in kernel mode
- long sTime = Long.parseLong(stats[14]);
- // Amount of clock ticks this process' waited-for children has been scheduled in user mode
- long cuTime = Long.parseLong(stats[15]);
- // Amount of clock ticks this process' waited-for children has been scheduled in kernel mode
- long csTime = Long.parseLong(stats[16]);
- return (long) ((uTime + sTime + cuTime + csTime) * nanosecondsPerClockTick);
- } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
- logger.log(SentryLevel.ERROR, "Error parsing /proc/self/stat file.", e);
- return 0;
- }
- }
- return 0;
+ return Process.getElapsedCpuTime() * NANOSECONDS_PER_MILLISECOND;
}
}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidLoggerBatchProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidLoggerBatchProcessor.java
new file mode 100644
index 00000000000..13b12dc702a
--- /dev/null
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidLoggerBatchProcessor.java
@@ -0,0 +1,47 @@
+package io.sentry.android.core;
+
+import io.sentry.ISentryClient;
+import io.sentry.SentryLevel;
+import io.sentry.SentryOptions;
+import io.sentry.logger.LoggerBatchProcessor;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+@ApiStatus.Internal
+public final class AndroidLoggerBatchProcessor extends LoggerBatchProcessor
+ implements AppState.AppStateListener {
+
+ public AndroidLoggerBatchProcessor(
+ @NotNull SentryOptions options, @NotNull ISentryClient client) {
+ super(options, client);
+ AppState.getInstance().addAppStateListener(this);
+ }
+
+ @Override
+ public void onForeground() {
+ // no-op
+ }
+
+ @Override
+ public void onBackground() {
+ try {
+ options
+ .getExecutorService()
+ .submit(
+ new Runnable() {
+ @Override
+ public void run() {
+ flush(LoggerBatchProcessor.FLUSH_AFTER_MS);
+ }
+ });
+ } catch (Throwable t) {
+ options.getLogger().log(SentryLevel.ERROR, t, "Failed to submit log flush in onBackground()");
+ }
+ }
+
+ @Override
+ public void close(boolean isRestarting) {
+ AppState.getInstance().removeAppStateListener(this);
+ super.close(isRestarting);
+ }
+}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidLoggerBatchProcessorFactory.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidLoggerBatchProcessorFactory.java
new file mode 100644
index 00000000000..694f94c7f7b
--- /dev/null
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidLoggerBatchProcessorFactory.java
@@ -0,0 +1,15 @@
+package io.sentry.android.core;
+
+import io.sentry.SentryClient;
+import io.sentry.SentryOptions;
+import io.sentry.logger.ILoggerBatchProcessor;
+import io.sentry.logger.ILoggerBatchProcessorFactory;
+import org.jetbrains.annotations.NotNull;
+
+public final class AndroidLoggerBatchProcessorFactory implements ILoggerBatchProcessorFactory {
+ @Override
+ public @NotNull ILoggerBatchProcessor create(
+ @NotNull SentryOptions options, @NotNull SentryClient client) {
+ return new AndroidLoggerBatchProcessor(options, client);
+ }
+}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessor.java
new file mode 100644
index 00000000000..290f2a9d4ed
--- /dev/null
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessor.java
@@ -0,0 +1,49 @@
+package io.sentry.android.core;
+
+import io.sentry.ISentryClient;
+import io.sentry.SentryLevel;
+import io.sentry.SentryOptions;
+import io.sentry.metrics.MetricsBatchProcessor;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+@ApiStatus.Internal
+public final class AndroidMetricsBatchProcessor extends MetricsBatchProcessor
+ implements AppState.AppStateListener {
+
+ public AndroidMetricsBatchProcessor(
+ final @NotNull SentryOptions options, final @NotNull ISentryClient client) {
+ super(options, client);
+ AppState.getInstance().addAppStateListener(this);
+ }
+
+ @Override
+ public void onForeground() {
+ // no-op
+ }
+
+ @Override
+ public void onBackground() {
+ try {
+ options
+ .getExecutorService()
+ .submit(
+ new Runnable() {
+ @Override
+ public void run() {
+ flush(MetricsBatchProcessor.FLUSH_AFTER_MS);
+ }
+ });
+ } catch (Throwable t) {
+ options
+ .getLogger()
+ .log(SentryLevel.ERROR, t, "Failed to submit metrics flush in onBackground()");
+ }
+ }
+
+ @Override
+ public void close(boolean isRestarting) {
+ AppState.getInstance().removeAppStateListener(this);
+ super.close(isRestarting);
+ }
+}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessorFactory.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessorFactory.java
new file mode 100644
index 00000000000..319440c27a9
--- /dev/null
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidMetricsBatchProcessorFactory.java
@@ -0,0 +1,15 @@
+package io.sentry.android.core;
+
+import io.sentry.SentryClient;
+import io.sentry.SentryOptions;
+import io.sentry.metrics.IMetricsBatchProcessor;
+import io.sentry.metrics.IMetricsBatchProcessorFactory;
+import org.jetbrains.annotations.NotNull;
+
+public final class AndroidMetricsBatchProcessorFactory implements IMetricsBatchProcessorFactory {
+ @Override
+ public @NotNull IMetricsBatchProcessor create(
+ final @NotNull SentryOptions options, final @NotNull SentryClient client) {
+ return new AndroidMetricsBatchProcessor(options, client);
+ }
+}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java
index 296916bb9ef..a0547a78b34 100644
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java
@@ -2,9 +2,12 @@
import static io.sentry.android.core.NdkIntegration.SENTRY_NDK_CLASS_NAME;
+import android.annotation.SuppressLint;
import android.app.Application;
import android.content.Context;
import android.content.pm.PackageInfo;
+import android.os.Build;
+import io.sentry.CompositePerformanceCollector;
import io.sentry.DeduplicateMultithreadedEventProcessor;
import io.sentry.DefaultCompositePerformanceCollector;
import io.sentry.DefaultVersionDetector;
@@ -15,6 +18,7 @@
import io.sentry.NoOpCompositePerformanceCollector;
import io.sentry.NoOpConnectionStatusProvider;
import io.sentry.NoOpContinuousProfiler;
+import io.sentry.NoOpReplayBreadcrumbConverter;
import io.sentry.NoOpSocketTagger;
import io.sentry.NoOpTransactionProfiler;
import io.sentry.NoopVersionDetector;
@@ -23,6 +27,8 @@
import io.sentry.SendFireAndForgetOutboxSender;
import io.sentry.SentryLevel;
import io.sentry.SentryOpenTelemetryMode;
+import io.sentry.android.core.anr.AnrProfileRotationHelper;
+import io.sentry.android.core.anr.AnrProfilingIntegration;
import io.sentry.android.core.cache.AndroidEnvelopeCache;
import io.sentry.android.core.internal.debugmeta.AssetsDebugMetaLoader;
import io.sentry.android.core.internal.gestures.AndroidViewGestureTargetLocator;
@@ -45,6 +51,7 @@
import io.sentry.internal.gestures.GestureTargetLocator;
import io.sentry.internal.modules.NoOpModulesLoader;
import io.sentry.internal.viewhierarchy.ViewHierarchyExporter;
+import io.sentry.protocol.SentryId;
import io.sentry.transport.CurrentDateProvider;
import io.sentry.transport.NoOpEnvelopeCache;
import io.sentry.transport.NoOpTransportGate;
@@ -106,7 +113,7 @@ static void loadDefaultAndMetadataOptions(
final @NotNull BuildInfoProvider buildInfoProvider) {
Objects.requireNonNull(context, "The context is required.");
- context = ContextUtils.getApplicationContext(context);
+ @NotNull final Context finalContext = ContextUtils.getApplicationContext(context);
Objects.requireNonNull(options, "The options object is required.");
Objects.requireNonNull(logger, "The ILogger object is required.");
@@ -118,18 +125,24 @@ static void loadDefaultAndMetadataOptions(
options.setDefaultScopeType(ScopeType.CURRENT);
options.setOpenTelemetryMode(SentryOpenTelemetryMode.OFF);
options.setDateProvider(new SentryAndroidDateProvider());
+ options.getLogs().setLoggerBatchProcessorFactory(new AndroidLoggerBatchProcessorFactory());
+ options.getMetrics().setMetricsBatchProcessorFactory(new AndroidMetricsBatchProcessorFactory());
// set a lower flush timeout on Android to avoid ANRs
options.setFlushTimeoutMillis(DEFAULT_FLUSH_TIMEOUT_MS);
options.setFrameMetricsCollector(
- new SentryFrameMetricsCollector(context, logger, buildInfoProvider));
+ new SentryFrameMetricsCollector(finalContext, logger, buildInfoProvider));
- ManifestMetadataReader.applyMetadata(context, options, buildInfoProvider);
- options.setCacheDirPath(getCacheDir(context).getAbsolutePath());
+ ManifestMetadataReader.applyMetadata(finalContext, options, buildInfoProvider);
- readDefaultOptionValues(options, context, buildInfoProvider);
+ options.setCacheDirPath(getCacheDir(finalContext).getAbsolutePath());
+
+ AnrProfileRotationHelper.rotate();
+
+ readDefaultOptionValues(options, finalContext, buildInfoProvider);
AppState.getInstance().registerLifecycleObserver(options);
+ options.activate();
}
@TestOnly
@@ -137,13 +150,15 @@ static void initializeIntegrationsAndProcessors(
final @NotNull SentryAndroidOptions options,
final @NotNull Context context,
final @NotNull io.sentry.util.LoadClass loadClass,
- final @NotNull ActivityFramesTracker activityFramesTracker) {
+ final @NotNull ActivityFramesTracker activityFramesTracker,
+ final boolean isReplayAvailable) {
initializeIntegrationsAndProcessors(
options,
context,
new BuildInfoProvider(new AndroidLogger()),
loadClass,
- activityFramesTracker);
+ activityFramesTracker,
+ isReplayAvailable);
}
static void initializeIntegrationsAndProcessors(
@@ -151,7 +166,8 @@ static void initializeIntegrationsAndProcessors(
final @NotNull Context context,
final @NotNull BuildInfoProvider buildInfoProvider,
final @NotNull io.sentry.util.LoadClass loadClass,
- final @NotNull ActivityFramesTracker activityFramesTracker) {
+ final @NotNull ActivityFramesTracker activityFramesTracker,
+ final boolean isReplayAvailable) {
if (options.getCacheDirPath() != null
&& options.getEnvelopeDiskCache() instanceof NoOpEnvelopeCache) {
@@ -167,41 +183,31 @@ static void initializeIntegrationsAndProcessors(
if (options.getCacheDirPath() != null) {
options.addScopeObserver(new PersistingScopeObserver(options));
options.addOptionsObserver(new PersistingOptionsObserver(options));
+ final PackageInfo packageInfo = ContextUtils.getPackageInfo(context, buildInfoProvider);
+ if (packageInfo != null && packageInfo.lastUpdateTime > 0) {
+ options.addOptionsObserver(
+ new PersistingOptionsCacheGenerationObserver(options, packageInfo.lastUpdateTime));
+ }
}
options.addEventProcessor(new DeduplicateMultithreadedEventProcessor(options));
options.addEventProcessor(
new DefaultAndroidEventProcessor(context, buildInfoProvider, options));
options.addEventProcessor(new PerformanceAndroidEventProcessor(options, activityFramesTracker));
- options.addEventProcessor(new ScreenshotEventProcessor(options, buildInfoProvider));
+ options.addEventProcessor(
+ new ScreenshotEventProcessor(options, buildInfoProvider, isReplayAvailable));
options.addEventProcessor(new ViewHierarchyEventProcessor(options));
- options.addEventProcessor(new AnrV2EventProcessor(context, options, buildInfoProvider));
+ options.addEventProcessor(
+ new ApplicationExitInfoEventProcessor(context, options, buildInfoProvider));
if (options.getTransportGate() instanceof NoOpTransportGate) {
options.setTransportGate(new AndroidTransportGate(options));
}
- // Check if the profiler was already instantiated in the app start.
- // We use the Android profiler, that uses a global start/stop api, so we need to preserve the
- // state of the profiler, and it's only possible retaining the instance.
final @NotNull AppStartMetrics appStartMetrics = AppStartMetrics.getInstance();
- final @Nullable ITransactionProfiler appStartTransactionProfiler;
- final @Nullable IContinuousProfiler appStartContinuousProfiler;
- try (final @NotNull ISentryLifecycleToken ignored = AppStartMetrics.staticLock.acquire()) {
- appStartTransactionProfiler = appStartMetrics.getAppStartProfiler();
- appStartContinuousProfiler = appStartMetrics.getAppStartContinuousProfiler();
- appStartMetrics.setAppStartProfiler(null);
- appStartMetrics.setAppStartContinuousProfiler(null);
- }
-
- setupProfiler(
- options,
- context,
- buildInfoProvider,
- appStartTransactionProfiler,
- appStartContinuousProfiler);
+ options.setAppStartExtender(appStartMetrics.getAppStartExtension());
if (options.getModulesLoader() instanceof NoOpModulesLoader) {
- options.setModulesLoader(new AssetsModulesLoader(context, options.getLogger()));
+ options.setModulesLoader(new AssetsModulesLoader(context, options));
}
if (options.getDebugMetaLoader() instanceof NoOpDebugMetaLoader) {
options.setDebugMetaLoader(new AssetsDebugMetaLoader(context, options.getLogger()));
@@ -210,8 +216,8 @@ static void initializeIntegrationsAndProcessors(
options.setVersionDetector(new DefaultVersionDetector(options));
}
- final boolean isAndroidXScrollViewAvailable =
- loadClass.isClassAvailable("androidx.core.view.ScrollingView", options);
+ final @NotNull LazyEvaluator isAndroidXScrollViewAvailable =
+ loadClass.isClassAvailableLazy("androidx.core.view.ScrollingView", options);
final boolean isComposeUpstreamAvailable =
loadClass.isClassAvailable(COMPOSE_CLASS_NAME, options);
@@ -246,6 +252,7 @@ static void initializeIntegrationsAndProcessors(
if (options.getSocketTagger() instanceof NoOpSocketTagger) {
options.setSocketTagger(AndroidSocketTagger.getInstance());
}
+
if (options.getPerformanceCollectors().isEmpty()) {
options.addPerformanceCollector(new AndroidMemoryCollector());
options.addPerformanceCollector(new AndroidCpuCollector(options.getLogger()));
@@ -262,17 +269,69 @@ static void initializeIntegrationsAndProcessors(
if (options.getCompositePerformanceCollector() instanceof NoOpCompositePerformanceCollector) {
options.setCompositePerformanceCollector(new DefaultCompositePerformanceCollector(options));
}
+
+ if (isReplayAvailable
+ && options.getReplayController().getBreadcrumbConverter()
+ instanceof NoOpReplayBreadcrumbConverter) {
+ options
+ .getReplayController()
+ .setBreadcrumbConverter(new DefaultReplayBreadcrumbConverter(options));
+ }
+
+ // Check if the profiler was already instantiated in the app start.
+ // We use the Android profiler, that uses a global start/stop api, so we need to preserve the
+ // state of the profiler, and it's only possible retaining the instance.
+ final @Nullable ITransactionProfiler appStartTransactionProfiler;
+ final @Nullable IContinuousProfiler appStartContinuousProfiler;
+ try (final @NotNull ISentryLifecycleToken ignored = AppStartMetrics.staticLock.acquire()) {
+ appStartTransactionProfiler = appStartMetrics.getAppStartProfiler();
+ appStartContinuousProfiler = appStartMetrics.getAppStartContinuousProfiler();
+ appStartMetrics.setAppStartProfiler(null);
+ appStartMetrics.setAppStartContinuousProfiler(null);
+ }
+
+ setupProfiler(
+ options,
+ context,
+ buildInfoProvider,
+ appStartTransactionProfiler,
+ appStartContinuousProfiler,
+ options.getCompositePerformanceCollector());
}
/** Setup the correct profiler (transaction or continuous) based on the options. */
+ @SuppressLint("NewApi")
private static void setupProfiler(
final @NotNull SentryAndroidOptions options,
final @NotNull Context context,
final @NotNull BuildInfoProvider buildInfoProvider,
final @Nullable ITransactionProfiler appStartTransactionProfiler,
- final @Nullable IContinuousProfiler appStartContinuousProfiler) {
+ final @Nullable IContinuousProfiler appStartContinuousProfiler,
+ final @NotNull CompositePerformanceCollector performanceCollector) {
if (options.isProfilingEnabled() || options.getProfilesSampleRate() != null) {
options.setContinuousProfiler(NoOpContinuousProfiler.getInstance());
+ // Transaction-based profiling always relies on the legacy Debug-based profiler, so it is
+ // disabled together with legacy profiling. Perfetto profiling only supports continuous
+ // profiling.
+ if (!options.isEnableLegacyProfiling()) {
+ options
+ .getLogger()
+ .log(
+ SentryLevel.WARNING,
+ "Transaction-based profiling (profilesSampleRate/profilesSampler) is disabled "
+ + "because enableLegacyProfiling is false. Transaction-based profiling always "
+ + "uses the legacy profiler and is not supported by Perfetto. No profiling "
+ + "data will be collected. Use profileSessionSampleRate for continuous "
+ + "profiling instead.");
+ options.setTransactionProfiler(NoOpTransactionProfiler.getInstance());
+ if (appStartTransactionProfiler != null) {
+ appStartTransactionProfiler.close();
+ }
+ if (appStartContinuousProfiler != null) {
+ appStartContinuousProfiler.close(true);
+ }
+ return;
+ }
// This is a safeguard, but it should never happen, as the app start profiler should be the
// continuous one.
if (appStartContinuousProfiler != null) {
@@ -299,17 +358,43 @@ private static void setupProfiler(
}
if (appStartContinuousProfiler != null) {
options.setContinuousProfiler(appStartContinuousProfiler);
+ // If the profiler is running, we start the performance collector too, otherwise we'd miss
+ // measurements in app launch profiles
+ final @NotNull SentryId chunkId = appStartContinuousProfiler.getChunkId();
+ if (appStartContinuousProfiler.isRunning() && !chunkId.equals(SentryId.EMPTY_ID)) {
+ performanceCollector.start(chunkId.toString());
+ }
} else {
- options.setContinuousProfiler(
- new AndroidContinuousProfiler(
- buildInfoProvider,
- Objects.requireNonNull(
- options.getFrameMetricsCollector(),
- "options.getFrameMetricsCollector is required"),
- options.getLogger(),
- options.getProfilingTracesDirPath(),
- options.getProfilingTracesHz(),
- options.getExecutorService()));
+ final @NotNull SentryFrameMetricsCollector frameMetricsCollector =
+ Objects.requireNonNull(
+ options.getFrameMetricsCollector(), "options.getFrameMetricsCollector is required");
+ if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ final @NotNull Context appContext = ContextUtils.getApplicationContext(context);
+ options.setContinuousProfiler(
+ new PerfettoContinuousProfiler(
+ options.getLogger(),
+ frameMetricsCollector,
+ () -> options.getExecutorService(),
+ () ->
+ new PerfettoProfiler(
+ appContext, options.getLogger(), options.getExecutorService())));
+ } else if (options.isEnableLegacyProfiling()) {
+ options.setContinuousProfiler(
+ new AndroidContinuousProfiler(
+ buildInfoProvider,
+ frameMetricsCollector,
+ options.getLogger(),
+ options.getProfilingTracesDirPath(),
+ options.getProfilingTracesHz(),
+ () -> options.getExecutorService()));
+ } else {
+ options
+ .getLogger()
+ .log(
+ SentryLevel.WARNING,
+ "enableLegacyProfiling is disabled and device is below API 35. "
+ + "No profiling data will be collected.");
+ }
}
}
}
@@ -343,6 +428,10 @@ static void installDefaultIntegrations(
final Class> sentryNdkClass = loadClass.loadClass(SENTRY_NDK_CLASS_NAME, options.getLogger());
options.addIntegration(new NdkIntegration(sentryNdkClass));
+ if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.S) {
+ options.addIntegration(new TombstoneIntegration(context));
+ }
+
// this integration uses android.os.FileObserver, we can't move to sentry
// before creating a pure java impl.
options.addIntegration(EnvelopeFileObserverIntegration.getOutboxFileObserver());
@@ -362,6 +451,8 @@ static void installDefaultIntegrations(
// it to set the replayId in case of an ANR
options.addIntegration(AnrIntegrationFactory.create(context, buildInfoProvider));
+ options.addIntegration(new AnrProfilingIntegration());
+
// registerActivityLifecycleCallbacks is only available if Context is an AppContext
if (context instanceof Application) {
options.addIntegration(
@@ -369,6 +460,7 @@ static void installDefaultIntegrations(
(Application) context, buildInfoProvider, activityFramesTracker));
options.addIntegration(new ActivityBreadcrumbsIntegration((Application) context));
options.addIntegration(new UserInteractionIntegration((Application) context, loadClass));
+ options.addIntegration(new FeedbackShakeIntegration((Application) context));
if (isFragmentAvailable) {
options.addIntegration(new FragmentLifecycleIntegration((Application) context, true, true));
}
@@ -389,7 +481,6 @@ static void installDefaultIntegrations(
if (isReplayAvailable) {
final ReplayIntegration replay =
new ReplayIntegration(context, CurrentDateProvider.getInstance());
- replay.setBreadcrumbConverter(new DefaultReplayBreadcrumbConverter());
options.addIntegration(replay);
options.setReplayController(replay);
}
@@ -400,7 +491,7 @@ static void installDefaultIntegrations(
}
options
.getFeedbackOptions()
- .setDialogHandler(new SentryAndroidOptions.AndroidUserFeedbackIDialogHandler());
+ .setFormHandler(new SentryAndroidOptions.AndroidUserFeedbackFormHandler());
}
/**
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java
index c6772529816..3f569df5378 100644
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidProfiler.java
@@ -16,6 +16,7 @@
import io.sentry.profilemeasurements.ProfileMeasurement;
import io.sentry.profilemeasurements.ProfileMeasurementValue;
import io.sentry.util.AutoClosableReentrantLock;
+import io.sentry.util.LazyEvaluator;
import io.sentry.util.Objects;
import java.io.File;
import java.util.ArrayDeque;
@@ -92,23 +93,25 @@ public ProfileEndData(
private final @NotNull ArrayDeque frozenFrameRenderMeasurements =
new ArrayDeque<>();
private final @NotNull Map measurementsMap = new HashMap<>();
- private final @Nullable ISentryExecutorService timeoutExecutorService;
+ private final @Nullable LazyEvaluator.Evaluator
+ timeoutExecutorServiceSupplier;
private final @NotNull ILogger logger;
- private boolean isRunning = false;
+ private volatile boolean isRunning = false;
protected final @NotNull AutoClosableReentrantLock lock = new AutoClosableReentrantLock();
public AndroidProfiler(
final @NotNull String tracesFilesDirPath,
final int intervalUs,
final @NotNull SentryFrameMetricsCollector frameMetricsCollector,
- final @Nullable ISentryExecutorService timeoutExecutorService,
+ final @Nullable LazyEvaluator.Evaluator
+ timeoutExecutorServiceSupplier,
final @NotNull ILogger logger) {
this.traceFilesDir =
new File(Objects.requireNonNull(tracesFilesDirPath, "TracesFilesDirPath is required"));
this.intervalUs = intervalUs;
this.logger = Objects.requireNonNull(logger, "Logger is required");
// Timeout executor is nullable, as timeouts will not be there for continuous profiling
- this.timeoutExecutorService = timeoutExecutorService;
+ this.timeoutExecutorServiceSupplier = timeoutExecutorServiceSupplier;
this.frameMetricsCollector =
Objects.requireNonNull(frameMetricsCollector, "SentryFrameMetricsCollector is required");
}
@@ -185,10 +188,11 @@ public void onFrameMetricCollected(
// We stop profiling after a timeout to avoid huge profiles to be sent
try {
- if (timeoutExecutorService != null) {
+ if (timeoutExecutorServiceSupplier != null) {
scheduledFinish =
- timeoutExecutorService.schedule(
- () -> endAndCollect(true, null), PROFILING_TIMEOUT_MILLIS);
+ timeoutExecutorServiceSupplier
+ .evaluate()
+ .schedule(() -> endAndCollect(true, null), PROFILING_TIMEOUT_MILLIS);
}
} catch (RejectedExecutionException e) {
logger.log(
@@ -318,21 +322,21 @@ private void putPerformanceCollectionDataInMeasurements(
for (final @NotNull PerformanceCollectionData data : performanceCollectionData) {
final long nanoTimestamp = data.getNanoTimestamp();
final long relativeStartNs = nanoTimestamp + timestampDiff;
- final @Nullable Double cpuUsagePercentage = data.getCpuUsagePercentage();
- final @Nullable Long usedHeapMemory = data.getUsedHeapMemory();
- final @Nullable Long usedNativeMemory = data.getUsedNativeMemory();
- if (cpuUsagePercentage != null) {
+ if (data.hasCpuUsagePercentage()) {
cpuUsageMeasurements.add(
- new ProfileMeasurementValue(relativeStartNs, cpuUsagePercentage, nanoTimestamp));
+ new ProfileMeasurementValue(
+ relativeStartNs, data.getCpuUsagePercentage(), nanoTimestamp));
}
- if (usedHeapMemory != null) {
+ if (data.hasUsedHeapMemory()) {
memoryUsageMeasurements.add(
- new ProfileMeasurementValue(relativeStartNs, usedHeapMemory, nanoTimestamp));
+ new ProfileMeasurementValue(
+ relativeStartNs, data.getUsedHeapMemory(), nanoTimestamp));
}
- if (usedNativeMemory != null) {
+ if (data.hasUsedNativeMemory()) {
nativeMemoryUsageMeasurements.add(
- new ProfileMeasurementValue(relativeStartNs, usedNativeMemory, nanoTimestamp));
+ new ProfileMeasurementValue(
+ relativeStartNs, data.getUsedNativeMemory(), nanoTimestamp));
}
}
}
@@ -354,4 +358,8 @@ private void putPerformanceCollectionDataInMeasurements(
}
}
}
+
+ boolean isRunning() {
+ return isRunning;
+ }
}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java
index 0aa678d5195..e44ed746a08 100644
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidTransactionProfiler.java
@@ -5,8 +5,6 @@
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
-import android.os.Process;
-import android.os.SystemClock;
import io.sentry.DateUtils;
import io.sentry.ILogger;
import io.sentry.ISentryExecutorService;
@@ -22,13 +20,14 @@
import io.sentry.android.core.internal.util.CpuInfoUtils;
import io.sentry.android.core.internal.util.SentryFrameMetricsCollector;
import io.sentry.util.AutoClosableReentrantLock;
+import io.sentry.util.LazyEvaluator;
import io.sentry.util.Objects;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
-import org.jetbrains.annotations.TestOnly;
final class AndroidTransactionProfiler implements ITransactionProfiler {
private final @NotNull Context context;
@@ -36,13 +35,19 @@ final class AndroidTransactionProfiler implements ITransactionProfiler {
private final @Nullable String profilingTracesDirPath;
private final boolean isProfilingEnabled;
private final int profilingTracesHz;
- private final @NotNull ISentryExecutorService executorService;
+ private final @NotNull LazyEvaluator.Evaluator executorServiceSupplier;
private final @NotNull BuildInfoProvider buildInfoProvider;
private boolean isInitialized = false;
- private int transactionsCounter = 0;
+ private final @NotNull AtomicBoolean isRunning = new AtomicBoolean(false);
private final @NotNull SentryFrameMetricsCollector frameMetricsCollector;
- private @Nullable ProfilingTransactionData currentProfilingTransactionData;
- private @Nullable AndroidProfiler profiler = null;
+ private volatile @Nullable ProfilingTransactionData currentProfilingTransactionData;
+
+ /**
+ * The underlying profiler instance. It is thread safe to call it after checking if it's not null,
+ * because we never nullify it after instantiation.
+ */
+ private volatile @Nullable AndroidProfiler profiler = null;
+
private long profileStartNanos;
private long profileStartCpuMillis;
private @NotNull Date profileStartTimestamp;
@@ -61,7 +66,7 @@ public AndroidTransactionProfiler(
sentryAndroidOptions.getProfilingTracesDirPath(),
sentryAndroidOptions.isProfilingEnabled(),
sentryAndroidOptions.getProfilingTracesHz(),
- sentryAndroidOptions.getExecutorService());
+ () -> sentryAndroidOptions.getExecutorService());
}
public AndroidTransactionProfiler(
@@ -73,6 +78,26 @@ public AndroidTransactionProfiler(
final boolean isProfilingEnabled,
final int profilingTracesHz,
final @NotNull ISentryExecutorService executorService) {
+ this(
+ context,
+ buildInfoProvider,
+ frameMetricsCollector,
+ logger,
+ profilingTracesDirPath,
+ isProfilingEnabled,
+ profilingTracesHz,
+ () -> executorService);
+ }
+
+ public AndroidTransactionProfiler(
+ final @NotNull Context context,
+ final @NotNull BuildInfoProvider buildInfoProvider,
+ final @NotNull SentryFrameMetricsCollector frameMetricsCollector,
+ final @NotNull ILogger logger,
+ final @Nullable String profilingTracesDirPath,
+ final boolean isProfilingEnabled,
+ final int profilingTracesHz,
+ final @NotNull LazyEvaluator.Evaluator executorServiceSupplier) {
this.context =
Objects.requireNonNull(
ContextUtils.getApplicationContext(context), "The application context is required");
@@ -84,8 +109,9 @@ public AndroidTransactionProfiler(
this.profilingTracesDirPath = profilingTracesDirPath;
this.isProfilingEnabled = isProfilingEnabled;
this.profilingTracesHz = profilingTracesHz;
- this.executorService =
- Objects.requireNonNull(executorService, "The ISentryExecutorService is required.");
+ this.executorServiceSupplier =
+ Objects.requireNonNull(
+ executorServiceSupplier, "A supplier for ISentryExecutorService is required.");
this.profileStartTimestamp = DateUtils.getCurrentDateTime();
}
@@ -95,6 +121,7 @@ private void init() {
return;
}
isInitialized = true;
+
if (!isProfilingEnabled) {
logger.log(SentryLevel.INFO, "Profiling is disabled in options.");
return;
@@ -118,28 +145,36 @@ private void init() {
profilingTracesDirPath,
(int) SECONDS.toMicros(1) / profilingTracesHz,
frameMetricsCollector,
- executorService,
+ executorServiceSupplier,
logger);
}
@Override
public void start() {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- // Debug.startMethodTracingSampling() is only available since Lollipop, but Android Profiler
- // causes crashes on api 21 -> https://github.com/getsentry/sentry-java/issues/3392
- if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) return;
+ // Debug.startMethodTracingSampling() is only available since Lollipop, but Android Profiler
+ // causes crashes on api 21 -> https://github.com/getsentry/sentry-java/issues/3392
+ if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) return;
+ // When the first transaction is starting, we can start profiling
+ if (!isRunning.getAndSet(true)) {
// Let's initialize trace folder and profiling interval
init();
- transactionsCounter++;
- // When the first transaction is starting, we can start profiling
- if (transactionsCounter == 1 && onFirstStart()) {
+ if (onFirstStart()) {
logger.log(SentryLevel.DEBUG, "Profiler started.");
} else {
- transactionsCounter--;
- logger.log(
- SentryLevel.WARNING, "A profile is already running. This profile will be ignored.");
+ // If profiler is not null and is running, it means that a profile is already running
+ if (profiler != null && profiler.isRunning()) {
+ logger.log(
+ SentryLevel.WARNING, "A profile is already running. This profile will be ignored.");
+ } else {
+ try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
+ // Ensure we unbind any transaction data, just in case of concurrent starts
+ currentProfilingTransactionData = null;
+ }
+ // Otherwise we update the flag, because it means the profiler is not running
+ isRunning.set(false);
+ }
}
}
}
@@ -164,11 +199,14 @@ private boolean onFirstStart() {
@Override
public void bindTransaction(final @NotNull ITransaction transaction) {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- // If the profiler is running, but no profilingTransactionData is set, we bind it here
- if (transactionsCounter > 0 && currentProfilingTransactionData == null) {
- currentProfilingTransactionData =
- new ProfilingTransactionData(transaction, profileStartNanos, profileStartCpuMillis);
+ // If the profiler is running, but no profilingTransactionData is set, we bind it here
+ if (isRunning.get() && currentProfilingTransactionData == null) {
+ try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
+ // If the profiler is running, but no profilingTransactionData is set, we bind it here
+ if (isRunning.get() && currentProfilingTransactionData == null) {
+ currentProfilingTransactionData =
+ new ProfilingTransactionData(transaction, profileStartNanos, profileStartCpuMillis);
+ }
}
}
}
@@ -178,15 +216,13 @@ public void bindTransaction(final @NotNull ITransaction transaction) {
final @NotNull ITransaction transaction,
final @Nullable List performanceCollectionData,
final @NotNull SentryOptions options) {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- return onTransactionFinish(
- transaction.getName(),
- transaction.getEventId().toString(),
- transaction.getSpanContext().getTraceId().toString(),
- false,
- performanceCollectionData,
- options);
- }
+ return onTransactionFinish(
+ transaction.getName(),
+ transaction.getEventId().toString(),
+ transaction.getSpanContext().getTraceId().toString(),
+ false,
+ performanceCollectionData,
+ options);
}
@SuppressLint("NewApi")
@@ -197,20 +233,23 @@ public void bindTransaction(final @NotNull ITransaction transaction) {
final boolean isTimeout,
final @Nullable List performanceCollectionData,
final @NotNull SentryOptions options) {
- try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
- // check if profiler was created
- if (profiler == null) {
- return null;
- }
- // onTransactionStart() is only available since Lollipop_MR1
- // and SystemClock.elapsedRealtimeNanos() since Jelly Bean
- // and SUPPORTED_ABIS since KITKAT
- if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) return null;
+ // onTransactionStart() is only available since Lollipop_MR1
+ // and SystemClock.elapsedRealtimeNanos() since Jelly Bean
+ // and SUPPORTED_ABIS since KITKAT
+ if (buildInfoProvider.getSdkInfoVersion() < Build.VERSION_CODES.LOLLIPOP_MR1) return null;
+
+ // check if profiler was created
+ if (profiler == null) {
+ return null;
+ }
+
+ final ProfilingTransactionData txData;
+ try (final @NotNull ISentryLifecycleToken ignored = lock.acquire()) {
+ txData = currentProfilingTransactionData;
// Transaction finished, but it's not in the current profile
- if (currentProfilingTransactionData == null
- || !currentProfilingTransactionData.getId().equals(transactionId)) {
+ if (txData == null || !txData.getId().equals(transactionId)) {
// A transaction is finishing, but it's not profiled. We can skip it
logger.log(
SentryLevel.INFO,
@@ -219,118 +258,90 @@ public void bindTransaction(final @NotNull ITransaction transaction) {
traceId);
return null;
}
+ currentProfilingTransactionData = null;
+ }
- if (transactionsCounter > 0) {
- transactionsCounter--;
- }
+ logger.log(SentryLevel.DEBUG, "Transaction %s (%s) finished.", transactionName, traceId);
- logger.log(SentryLevel.DEBUG, "Transaction %s (%s) finished.", transactionName, traceId);
+ final AndroidProfiler.ProfileEndData endData =
+ profiler.endAndCollect(false, performanceCollectionData);
- if (transactionsCounter != 0) {
- // We notify the data referring to this transaction that it finished
- if (currentProfilingTransactionData != null) {
- currentProfilingTransactionData.notifyFinish(
- SystemClock.elapsedRealtimeNanos(),
- profileStartNanos,
- Process.getElapsedCpuTime(),
- profileStartCpuMillis);
- }
- return null;
- }
-
- final AndroidProfiler.ProfileEndData endData =
- profiler.endAndCollect(false, performanceCollectionData);
- // check if profiler end successfully
- if (endData == null) {
- return null;
- }
+ isRunning.set(false);
- long transactionDurationNanos = endData.endNanos - profileStartNanos;
+ // check if profiler end successfully
+ if (endData == null) {
+ return null;
+ }
- List transactionList = new ArrayList<>(1);
- final ProfilingTransactionData txData = currentProfilingTransactionData;
- if (txData != null) {
- transactionList.add(txData);
- }
- currentProfilingTransactionData = null;
- // We clear the counter in case of a timeout
- transactionsCounter = 0;
-
- String totalMem = "0";
- final @Nullable Long memory =
- (options instanceof SentryAndroidOptions)
- ? DeviceInfoUtil.getInstance(context, (SentryAndroidOptions) options).getTotalMemory()
- : null;
- if (memory != null) {
- totalMem = Long.toString(memory);
- }
- String[] abis = Build.SUPPORTED_ABIS;
+ long transactionDurationNanos = endData.endNanos - profileStartNanos;
- // We notify all transactions data that all transactions finished.
- // Some may not have been really finished, in case of a timeout
- for (ProfilingTransactionData t : transactionList) {
- t.notifyFinish(
- endData.endNanos, profileStartNanos, endData.endCpuMillis, profileStartCpuMillis);
- }
+ final @NotNull List transactionList = new ArrayList<>(1);
+ transactionList.add(txData);
+ txData.notifyFinish(
+ endData.endNanos, profileStartNanos, endData.endCpuMillis, profileStartCpuMillis);
- // cpu max frequencies are read with a lambda because reading files is involved, so it will be
- // done in the background when the trace file is read
- return new ProfilingTraceData(
- endData.traceFile,
- profileStartTimestamp,
- transactionList,
- transactionName,
- transactionId,
- traceId,
- Long.toString(transactionDurationNanos),
- buildInfoProvider.getSdkInfoVersion(),
- abis != null && abis.length > 0 ? abis[0] : "",
- () -> CpuInfoUtils.getInstance().readMaxFrequencies(),
- buildInfoProvider.getManufacturer(),
- buildInfoProvider.getModel(),
- buildInfoProvider.getVersionRelease(),
- buildInfoProvider.isEmulator(),
- totalMem,
- options.getProguardUuid(),
- options.getRelease(),
- options.getEnvironment(),
- (endData.didTimeout || isTimeout)
- ? ProfilingTraceData.TRUNCATION_REASON_TIMEOUT
- : ProfilingTraceData.TRUNCATION_REASON_NORMAL,
- endData.measurementsMap);
+ String totalMem = "0";
+ final @Nullable Long memory =
+ (options instanceof SentryAndroidOptions)
+ ? DeviceInfoUtil.getInstance(context, (SentryAndroidOptions) options).getTotalMemory()
+ : null;
+ if (memory != null) {
+ totalMem = Long.toString(memory);
}
+ final String[] abis = Build.SUPPORTED_ABIS;
+
+ // cpu max frequencies are read with a lambda because reading files is involved, so it will be
+ // done in the background when the trace file is read
+ return new ProfilingTraceData(
+ endData.traceFile,
+ profileStartTimestamp,
+ transactionList,
+ transactionName,
+ transactionId,
+ traceId,
+ Long.toString(transactionDurationNanos),
+ buildInfoProvider.getSdkInfoVersion(),
+ abis != null && abis.length > 0 ? abis[0] : "",
+ () -> CpuInfoUtils.getInstance().readMaxFrequencies(),
+ buildInfoProvider.getManufacturer(),
+ buildInfoProvider.getModel(),
+ buildInfoProvider.getVersionRelease(),
+ buildInfoProvider.isEmulator(),
+ totalMem,
+ options.getProguardUuid(),
+ options.getRelease(),
+ options.getEnvironment(),
+ (endData.didTimeout || isTimeout)
+ ? ProfilingTraceData.TRUNCATION_REASON_TIMEOUT
+ : ProfilingTraceData.TRUNCATION_REASON_NORMAL,
+ endData.measurementsMap);
}
@Override
public boolean isRunning() {
- return transactionsCounter != 0;
+ return isRunning.get();
}
@Override
public void close() {
+ final @Nullable ProfilingTransactionData txData = currentProfilingTransactionData;
// we stop profiling
- if (currentProfilingTransactionData != null) {
+ if (txData != null) {
onTransactionFinish(
- currentProfilingTransactionData.getName(),
- currentProfilingTransactionData.getId(),
- currentProfilingTransactionData.getTraceId(),
+ txData.getName(),
+ txData.getId(),
+ txData.getTraceId(),
true,
null,
ScopesAdapter.getInstance().getOptions());
- } else if (transactionsCounter != 0) {
- // in case the app start profiling is running, and it's not bound to a transaction, we still
- // stop profiling, but we also have to manually update the counter.
- transactionsCounter--;
}
+ // in case the app start profiling is running, and it's not bound to a transaction, we still
+ // stop profiling, but we also have to manually update the flag.
+ isRunning.set(false);
// we have to first stop profiling otherwise we would lost the last profile
if (profiler != null) {
profiler.close();
}
}
-
- @TestOnly
- int getTransactionsCounter() {
- return transactionsCounter;
- }
}
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java
index 8243493a50b..f37d433d308 100644
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java
+++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java
@@ -139,11 +139,18 @@ void reportANR(
message = "Background " + message;
}
- final ApplicationNotResponding error = new ApplicationNotResponding(message, anr.getThread());
+ final @Nullable Thread thread = anr.getThread();
+ final @NotNull ApplicationNotResponding error;
+ if (thread == null) {
+ error = new ApplicationNotResponding(message);
+ } else {
+ error = new ApplicationNotResponding(message, thread);
+ }
+
final Mechanism mechanism = new Mechanism();
mechanism.setType("ANR");
- return new ExceptionMechanismException(mechanism, error, error.getThread(), true);
+ return new ExceptionMechanismException(mechanism, error, thread, true);
}
@TestOnly
diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java
deleted file mode 100644
index 4710b2506da..00000000000
--- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrV2EventProcessor.java
+++ /dev/null
@@ -1,704 +0,0 @@
-package io.sentry.android.core;
-
-import static io.sentry.cache.PersistingOptionsObserver.DIST_FILENAME;
-import static io.sentry.cache.PersistingOptionsObserver.ENVIRONMENT_FILENAME;
-import static io.sentry.cache.PersistingOptionsObserver.PROGUARD_UUID_FILENAME;
-import static io.sentry.cache.PersistingOptionsObserver.RELEASE_FILENAME;
-import static io.sentry.cache.PersistingOptionsObserver.REPLAY_ERROR_SAMPLE_RATE_FILENAME;
-import static io.sentry.cache.PersistingOptionsObserver.SDK_VERSION_FILENAME;
-import static io.sentry.cache.PersistingScopeObserver.BREADCRUMBS_FILENAME;
-import static io.sentry.cache.PersistingScopeObserver.CONTEXTS_FILENAME;
-import static io.sentry.cache.PersistingScopeObserver.EXTRAS_FILENAME;
-import static io.sentry.cache.PersistingScopeObserver.FINGERPRINT_FILENAME;
-import static io.sentry.cache.PersistingScopeObserver.LEVEL_FILENAME;
-import static io.sentry.cache.PersistingScopeObserver.REPLAY_FILENAME;
-import static io.sentry.cache.PersistingScopeObserver.REQUEST_FILENAME;
-import static io.sentry.cache.PersistingScopeObserver.TRACE_FILENAME;
-import static io.sentry.cache.PersistingScopeObserver.TRANSACTION_FILENAME;
-import static io.sentry.cache.PersistingScopeObserver.USER_FILENAME;
-import static io.sentry.protocol.Contexts.REPLAY_ID;
-
-import android.annotation.SuppressLint;
-import android.app.ActivityManager;
-import android.content.Context;
-import android.content.pm.PackageInfo;
-import android.os.Build;
-import android.util.DisplayMetrics;
-import androidx.annotation.WorkerThread;
-import io.sentry.BackfillingEventProcessor;
-import io.sentry.Breadcrumb;
-import io.sentry.Hint;
-import io.sentry.IpAddressUtils;
-import io.sentry.SentryBaseEvent;
-import io.sentry.SentryEvent;
-import io.sentry.SentryExceptionFactory;
-import io.sentry.SentryLevel;
-import io.sentry.SentryOptions;
-import io.sentry.SentryStackTraceFactory;
-import io.sentry.SpanContext;
-import io.sentry.android.core.internal.util.CpuInfoUtils;
-import io.sentry.cache.PersistingOptionsObserver;
-import io.sentry.cache.PersistingScopeObserver;
-import io.sentry.hints.AbnormalExit;
-import io.sentry.hints.Backfillable;
-import io.sentry.protocol.App;
-import io.sentry.protocol.Contexts;
-import io.sentry.protocol.DebugImage;
-import io.sentry.protocol.DebugMeta;
-import io.sentry.protocol.Device;
-import io.sentry.protocol.Mechanism;
-import io.sentry.protocol.OperatingSystem;
-import io.sentry.protocol.Request;
-import io.sentry.protocol.SdkVersion;
-import io.sentry.protocol.SentryStackTrace;
-import io.sentry.protocol.SentryThread;
-import io.sentry.protocol.SentryTransaction;
-import io.sentry.protocol.User;
-import io.sentry.util.HintUtils;
-import io.sentry.util.SentryRandom;
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import org.jetbrains.annotations.ApiStatus;
-import org.jetbrains.annotations.NotNull;
-import org.jetbrains.annotations.Nullable;
-
-/**
- * AnrV2Integration processes events on a background thread, hence the event processors will also be
- * invoked on the same background thread, so we can safely read data from disk synchronously.
- */
-@ApiStatus.Internal
-@WorkerThread
-public final class AnrV2EventProcessor implements BackfillingEventProcessor {
-
- private final @NotNull Context context;
-
- private final @NotNull SentryAndroidOptions options;
-
- private final @NotNull BuildInfoProvider buildInfoProvider;
-
- private final @NotNull SentryExceptionFactory sentryExceptionFactory;
-
- private final @Nullable PersistingScopeObserver persistingScopeObserver;
-
- public AnrV2EventProcessor(
- final @NotNull Context context,
- final @NotNull SentryAndroidOptions options,
- final @NotNull BuildInfoProvider buildInfoProvider) {
- this.context = ContextUtils.getApplicationContext(context);
- this.options = options;
- this.buildInfoProvider = buildInfoProvider;
- this.persistingScopeObserver = options.findPersistingScopeObserver();
-
- final SentryStackTraceFactory sentryStackTraceFactory =
- new SentryStackTraceFactory(this.options);
-
- sentryExceptionFactory = new SentryExceptionFactory(sentryStackTraceFactory);
- }
-
- @Override
- public @NotNull SentryTransaction process(
- @NotNull SentryTransaction transaction, @NotNull Hint hint) {
- // that's only necessary because on newer versions of Unity, if not overriding this method, it's
- // throwing 'java.lang.AbstractMethodError: abstract method' and the reason is probably
- // compilation mismatch
- return transaction;
- }
-
- @Override
- public @Nullable SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) {
- final Object unwrappedHint = HintUtils.getSentrySdkHint(hint);
- if (!(unwrappedHint instanceof Backfillable)) {
- options
- .getLogger()
- .log(
- SentryLevel.WARNING,
- "The event is not Backfillable, but has been passed to BackfillingEventProcessor, skipping.");
- return event;
- }
-
- // we always set exception values, platform, os and device even if the ANR is not enrich-able
- // even though the OS context may change in the meantime (OS update), we consider this an
- // edge-case
- setExceptions(event, unwrappedHint);
- setPlatform(event);
- mergeOS(event);
- setDevice(event);
-
- if (!((Backfillable) unwrappedHint).shouldEnrich()) {
- options
- .getLogger()
- .log(
- SentryLevel.DEBUG,
- "The event is Backfillable, but should not be enriched, skipping.");
- return event;
- }
-
- backfillScope(event, unwrappedHint);
-
- backfillOptions(event, unwrappedHint);
-
- setStaticValues(event);
-
- return event;
- }
-
- // region scope persisted values
- private void backfillScope(final @NotNull SentryEvent event, final @NotNull Object hint) {
- setRequest(event);
- setUser(event);
- setScopeTags(event);
- setBreadcrumbs(event);
- setExtras(event);
- setContexts(event);
- setTransaction(event);
- setFingerprints(event, hint);
- setLevel(event);
- setTrace(event);
- setReplayId(event);
- }
-
- private boolean sampleReplay(final @NotNull SentryEvent event) {
- final @Nullable String replayErrorSampleRate =
- PersistingOptionsObserver.read(options, REPLAY_ERROR_SAMPLE_RATE_FILENAME, String.class);
-
- if (replayErrorSampleRate == null) {
- return false;
- }
-
- try {
- // we have to sample here with the old sample rate, because it may change between app launches
- final double replayErrorSampleRateDouble = Double.parseDouble(replayErrorSampleRate);
- if (replayErrorSampleRateDouble < SentryRandom.current().nextDouble()) {
- options
- .getLogger()
- .log(
- SentryLevel.DEBUG,
- "Not capturing replay for ANR %s due to not being sampled.",
- event.getEventId());
- return false;
- }
- } catch (Throwable e) {
- options.getLogger().log(SentryLevel.ERROR, "Error parsing replay sample rate.", e);
- return false;
- }
-
- return true;
- }
-
- private void setReplayId(final @NotNull SentryEvent event) {
- @Nullable String persistedReplayId = readFromDisk(options, REPLAY_FILENAME, String.class);
- final @NotNull File replayFolder =
- new File(options.getCacheDirPath(), "replay_" + persistedReplayId);
- if (!replayFolder.exists()) {
- if (!sampleReplay(event)) {
- return;
- }
- // if the replay folder does not exist (e.g. running in buffer mode), we need to find the
- // latest replay folder that was modified before the ANR event.
- persistedReplayId = null;
- long lastModified = Long.MIN_VALUE;
- final File[] dirs = new File(options.getCacheDirPath()).listFiles();
- if (dirs != null) {
- for (File dir : dirs) {
- if (dir.isDirectory() && dir.getName().startsWith("replay_")) {
- if (dir.lastModified() > lastModified
- && dir.lastModified() <= event.getTimestamp().getTime()) {
- lastModified = dir.lastModified();
- persistedReplayId = dir.getName().substring("replay_".length());
- }
- }
- }
- }
- }
-
- if (persistedReplayId == null) {
- return;
- }
-
- // store the relevant replayId so ReplayIntegration can pick it up and finalize that replay
- PersistingScopeObserver.store(options, persistedReplayId, REPLAY_FILENAME);
- event.getContexts().put(REPLAY_ID, persistedReplayId);
- }
-
- private void setTrace(final @NotNull SentryEvent event) {
- final SpanContext spanContext = readFromDisk(options, TRACE_FILENAME, SpanContext.class);
- if (event.getContexts().getTrace() == null) {
- if (spanContext != null
- && spanContext.getSpanId() != null
- && spanContext.getTraceId() != null) {
- event.getContexts().setTrace(spanContext);
- }
- }
- }
-
- private void setLevel(final @NotNull SentryEvent event) {
- final SentryLevel level = readFromDisk(options, LEVEL_FILENAME, SentryLevel.class);
- if (event.getLevel() == null) {
- event.setLevel(level);
- }
- }
-
- @SuppressWarnings("unchecked")
- private void setFingerprints(final @NotNull SentryEvent event, final @NotNull Object hint) {
- final List fingerprint =
- (List) readFromDisk(options, FINGERPRINT_FILENAME, List.class);
- if (event.getFingerprints() == null) {
- event.setFingerprints(fingerprint);
- }
-
- // sentry does not yet have a capability to provide default server-side fingerprint rules,
- // so we're doing this on the SDK side to group background and foreground ANRs separately
- // even if they have similar stacktraces
- final boolean isBackgroundAnr = isBackgroundAnr(hint);
- if (event.getFingerprints() == null) {
- event.setFingerprints(
- Arrays.asList("{{ default }}", isBackgroundAnr ? "background-anr" : "foreground-anr"));
- }
- }
-
- private void setTransaction(final @NotNull SentryEvent event) {
- final String transaction = readFromDisk(options, TRANSACTION_FILENAME, String.class);
- if (event.getTransaction() == null) {
- event.setTransaction(transaction);
- }
- }
-
- private void setContexts(final @NotNull SentryBaseEvent event) {
- final Contexts persistedContexts = readFromDisk(options, CONTEXTS_FILENAME, Contexts.class);
- if (persistedContexts == null) {
- return;
- }
- final Contexts eventContexts = event.getContexts();
- for (Map.Entry entry : new Contexts(persistedContexts).entrySet()) {
- final Object value = entry.getValue();
- if (SpanContext.TYPE.equals(entry.getKey()) && value instanceof SpanContext) {
- // we fill it in setTrace later on
- continue;
- }
- if (!eventContexts.containsKey(entry.getKey())) {
- eventContexts.put(entry.getKey(), value);
- }
- }
- }
-
- @SuppressWarnings("unchecked")
- private void setExtras(final @NotNull SentryBaseEvent event) {
- final Map extras =
- (Map