Skip to content

Commit f1edc98

Browse files
authored
Merge branch 'main' into feat/strict-trace-continuation
2 parents 327d897 + e2dce0b commit f1edc98

File tree

101 files changed

+2433
-158
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

101 files changed

+2433
-158
lines changed

.claude/skills/create-java-pr/SKILL.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ Derive the branch name from the changes being made. Use `feat/`, `fix/`, `ref/`,
3535

3636
**For stacked PRs:** For the first PR in a new stack, first create and push the collection branch (see `.cursor/rules/pr.mdc` § "Creating the Collection Branch"), then branch the PR off it. For subsequent PRs, branch off the previous stack branch. Use the naming conventions from `.cursor/rules/pr.mdc` § "Branch Naming".
3737

38+
**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.
39+
3840
## Step 2: Format Code and Regenerate API Files
3941

4042
```bash
@@ -111,14 +113,21 @@ Fill in each section based on the changes being PR'd. Check any checklist items
111113
- Pass `--base <previous-stack-branch>` so the PR targets the previous branch (first PR in a stack targets the collection branch).
112114
- Use the stacked PR title format: `<type>(<scope>): [<Topic> <N>] <Subject>` (see `.cursor/rules/pr.mdc` § "PR Title Naming").
113115
- Include the stack list at the top of the PR body, before the `## :scroll: Description` section (see `.cursor/rules/pr.mdc` § "Stack List in PR Description" for the format).
116+
- Add a merge method reminder at the very end of the PR body (see `.cursor/rules/pr.mdc` § "Stack List in PR Description" for the exact text). This only applies to stack PRs, not the collection branch PR.
114117

115118
Then continue to Step 5.5 (stacked PRs only) or Step 6.
116119

117120
## Step 5.5: Update Stack List on All PRs (stacked PRs only)
118121

119122
Skip this step for standalone PRs.
120123

121-
After creating the PR, update the PR description on **every other PR in the stack** so all PRs have the same up-to-date stack list. Follow the format and commands in `.cursor/rules/pr.mdc` § "Stack List in PR Description".
124+
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 `.cursor/rules/pr.mdc` § "Stack List in PR Description".
125+
126+
**Important:** When updating PR bodies, never use shell redirects (`>`, `>>`) or pipes (`|`) or compound commands (`&&`). These create compound shell expressions that won't match permission patterns. Instead:
127+
- Use `gh pr view <NUMBER> --json body --jq '.body'` to get the body (output returned directly)
128+
- Use the `Write` tool to save it to a temp file
129+
- Use the `Edit` tool to modify the temp file
130+
- Use `gh pr edit <NUMBER> --body-file /tmp/pr-body.md` to update
122131

123132
## Step 6: Update Changelog
124133

@@ -170,8 +179,6 @@ git push
170179

171180
If no changelog entry is needed, add `#skip-changelog` to the PR description to disable the changelog CI check:
172181

173-
```bash
174-
gh pr view <PR_NUMBER> --json body --jq '.body' > /tmp/pr-body.md
175-
printf '\n#skip-changelog\n' >> /tmp/pr-body.md
176-
gh pr edit <PR_NUMBER> --body-file /tmp/pr-body.md
177-
```
182+
1. Get the current body: `gh pr view <PR_NUMBER> --json body --jq '.body'`
183+
2. Use the `Write` tool to save the output to `/tmp/pr-body.md`, appending `\n#skip-changelog\n` at the end
184+
3. Update: `gh pr edit <PR_NUMBER> --body-file /tmp/pr-body.md`
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
---
2+
alwaysApply: false
3+
description: JVM Continuous Profiling (sentry-async-profiler)
4+
---
5+
# JVM Continuous Profiling
6+
7+
Use this rule when working on JVM continuous profiling in `sentry-async-profiler` and the related core profiling abstractions in `sentry`.
8+
9+
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.
10+
11+
## Module Structure
12+
13+
- **`sentry-async-profiler`**: standalone module containing the async-profiler integration
14+
- Uses Java `ServiceLoader` discovery
15+
- No direct dependency from core `sentry` module
16+
- Enabled by adding the module as a dependency
17+
18+
- **`sentry` core abstractions**:
19+
- `IContinuousProfiler`: profiler lifecycle interface
20+
- `ProfileChunk`: profile chunk payload sent to Sentry
21+
- `IProfileConverter`: converts JVM JFR files into `SentryProfile`
22+
- `ProfileLifecycle`: controls MANUAL vs TRACE lifecycle
23+
- `ProfilingServiceLoader`: loads profiler and converter implementations via `ServiceLoader`
24+
25+
## Key Classes
26+
27+
### `JavaContinuousProfiler` (`sentry-async-profiler`)
28+
- Wraps the native async-profiler library
29+
- Writes JFR files to `profilingTracesDirPath`
30+
- Rotates chunks periodically via `MAX_CHUNK_DURATION_MILLIS` (currently 10s)
31+
- Implements `RateLimiter.IRateLimitObserver`
32+
- Maintains `rootSpanCounter` for TRACE lifecycle
33+
- Keeps a session-level `profilerId` across chunks until the profiling session ends
34+
- `getChunkId()` currently returns `SentryId.EMPTY_ID`, but emitted `ProfileChunk`s get a fresh chunk id when built in `stop(...)`
35+
36+
### `ProfileChunk`
37+
- Carries `profilerId`, `chunkId`, timestamp, platform, measurements, and a JFR file reference
38+
- Built via `ProfileChunk.Builder`
39+
- For JVM, the JFR file is converted later during envelope item creation, not inside `JavaContinuousProfiler`
40+
41+
### `ProfileLifecycle`
42+
- `MANUAL`: explicit `Sentry.startProfiler()` / `Sentry.stopProfiler()`
43+
- `TRACE`: profiler lifecycle follows active sampled root spans
44+
45+
## Configuration
46+
47+
Continuous profiling is **not** controlled by `profilesSampleRate`.
48+
49+
Key options:
50+
- **`profileSessionSampleRate`**: session-level sample rate for continuous profiling
51+
- **`profileLifecycle`**: `ProfileLifecycle.MANUAL` (default) or `ProfileLifecycle.TRACE`
52+
- **`cacheDirPath`**: base SDK cache directory; profiling traces are written under the derived `profilingTracesDirPath`
53+
- **`profilingTracesHz`**: sampling frequency in Hz (default: 101)
54+
55+
Continuous profiling is enabled when:
56+
- `profilesSampleRate == null`
57+
- `profilesSampler == null`
58+
- `profileSessionSampleRate != null && profileSessionSampleRate > 0`
59+
60+
Example:
61+
62+
```java
63+
options.setProfileSessionSampleRate(1.0);
64+
options.setCacheDirPath("/tmp/sentry-cache");
65+
options.setProfileLifecycle(ProfileLifecycle.MANUAL);
66+
options.setProfilingTracesHz(101);
67+
```
68+
69+
## How It Works
70+
71+
### Initialization
72+
- `InitUtil.initializeProfiler(...)` resolves or creates the profiling traces directory
73+
- `ProfilingServiceLoader.loadContinuousProfiler(...)` uses `ServiceLoader` to find `JavaContinuousProfilerProvider`
74+
- `AsyncProfilerContinuousProfilerProvider` instantiates `JavaContinuousProfiler`
75+
- `ProfilingServiceLoader.loadProfileConverter()` separately loads the `JavaProfileConverterProvider`
76+
77+
### Profiling Flow
78+
79+
**Start**
80+
- Sampling decision is made via `TracesSampler.sampleSessionProfile(...)`
81+
- Sampling is session-based and cached until `reevaluateSampling()`
82+
- Scopes and rate limiter are initialized lazily via `initScopes()`
83+
- Rate limits for `All` or `ProfileChunk` abort startup
84+
- JFR filename is generated under `profilingTracesDirPath`
85+
- async-profiler is started with a command like:
86+
- `start,jfr,event=wall,nobatch,interval=<interval>,file=<path>`
87+
- Automatic chunk stop is scheduled after `MAX_CHUNK_DURATION_MILLIS`
88+
89+
**Chunk Rotation**
90+
- `stop(true)` stops async-profiler and validates the JFR file
91+
- A `ProfileChunk.Builder` is created with:
92+
- current `profilerId`
93+
- a fresh `chunkId`
94+
- trace file
95+
- chunk timestamp
96+
- platform `java`
97+
- Builder is buffered in `payloadBuilders`
98+
- Chunks are sent if scopes are available
99+
- Profiling is restarted for the next chunk
100+
101+
**Stop**
102+
- `MANUAL`: stop immediately, do not restart, reset `profilerId`
103+
- `TRACE`: decrement `rootSpanCounter`; stop only when it reaches 0
104+
- `close(...)` also forces shutdown and resets TRACE state
105+
106+
### Sending and Conversion
107+
- `JavaContinuousProfiler` buffers `ProfileChunk.Builder` instances
108+
- `sendChunks(...)` builds `ProfileChunk` objects and calls `scopes.captureProfileChunk(...)`
109+
- `SentryClient.captureProfileChunk(...)` creates an envelope item
110+
- JVM JFR-to-`SentryProfile` conversion happens in `SentryEnvelopeItem.fromProfileChunk(...)` using the loaded `IProfileConverter`
111+
- Trace files are deleted in the envelope item path after serialization attempts
112+
113+
## TRACE Mode Lifecycle
114+
- `rootSpanCounter` increments when sampled root spans start
115+
- `rootSpanCounter` decrements when root spans finish
116+
- Profiler runs while `rootSpanCounter > 0`
117+
- Multiple concurrent sampled transactions can share the same profiling session
118+
- Be careful when changing lifecycle logic: this area is lock-protected and concurrency-sensitive
119+
120+
## Rate Limiting and Buffering
121+
122+
### Rate Limiting
123+
- Registers as a `RateLimiter.IRateLimitObserver`
124+
- If rate limited for `ProfileChunk` or `All`:
125+
- profiler stops immediately
126+
- it does not auto-restart when the limit expires
127+
- Startup also checks rate limiting before profiling begins
128+
129+
### Buffering / pre-init behavior
130+
- JFR files are written to `profilingTracesDirPath` and marked `deleteOnExit()` when a chunk is accepted
131+
- If scopes are not yet available, `ProfileChunk.Builder`s remain buffered in memory in `payloadBuilders`
132+
- This commonly matters for profiling that starts before SDK scopes are ready
133+
- 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
134+
135+
## Extending
136+
137+
To add or replace JVM profiler implementations:
138+
- implement `IContinuousProfiler`
139+
- implement `JavaContinuousProfilerProvider`
140+
- register provider in:
141+
- `META-INF/services/io.sentry.profiling.JavaContinuousProfilerProvider`
142+
143+
To add or replace JVM profile conversion:
144+
- implement `IProfileConverter`
145+
- implement `JavaProfileConverterProvider`
146+
- register provider in:
147+
- `META-INF/services/io.sentry.profiling.JavaProfileConverterProvider`
148+
149+
## Code Locations
150+
151+
Primary implementation:
152+
- `sentry/src/main/java/io/sentry/IContinuousProfiler.java`
153+
- `sentry/src/main/java/io/sentry/ProfileChunk.java`
154+
- `sentry/src/main/java/io/sentry/profiling/ProfilingServiceLoader.java`
155+
- `sentry/src/main/java/io/sentry/util/InitUtil.java`
156+
- `sentry/src/main/java/io/sentry/SentryEnvelopeItem.java`
157+
- `sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfiler.java`
158+
- `sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerContinuousProfilerProvider.java`
159+
- `sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/provider/AsyncProfilerProfileConverterProvider.java`
160+
- `sentry-async-profiler/src/main/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverter.java`
161+
162+
Tests to read first:
163+
- `sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/profiling/JavaContinuousProfilerTest.kt`
164+
- `sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/JavaContinuousProfilingServiceLoaderTest.kt`
165+
- `sentry-async-profiler/src/test/java/io/sentry/asyncprofiler/convert/JfrAsyncProfilerToSentryProfileConverterTest.kt`
166+
167+
## LLM Guidance
168+
169+
This rule is good enough for orientation, but for actual code changes always verify:
170+
- the sampling path in `TracesSampler`
171+
- continuous profiling enablement in `SentryOptions`
172+
- lifecycle entry points in `Scopes` and `SentryTracer`
173+
- conversion and file deletion behavior in `SentryEnvelopeItem`
174+
- existing tests before changing concurrency or lifecycle semantics

.cursor/rules/overview_dev.mdc

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Use the `fetch_rules` tool to include these rules when working on specific areas
3030

3131
- **`scopes`**: Use when working with:
3232
- Hub/Scope management, forking, or lifecycle
33-
- `Sentry.getCurrentScopes()`, `pushScope()`, `withScope()`
33+
- `Sentry.getCurrentScopes()`, `pushScope()`, `withScope()`
3434
- `ScopeType` (GLOBAL, ISOLATION, CURRENT)
3535
- Thread-local storage, scope bleeding issues
3636
- Migration from Hub API (v7 → v8)
@@ -66,6 +66,18 @@ Use the `fetch_rules` tool to include these rules when working on specific areas
6666
- `SentryMetricsEvent`, `SentryMetricsEvents`
6767
- `SentryOptions.getMetrics()`, `beforeSend` callback
6868

69+
- **`continuous_profiling_jvm`**: Use when working with:
70+
- JVM continuous profiling (`sentry-async-profiler` module)
71+
- `IContinuousProfiler`, `JavaContinuousProfiler`
72+
- `ProfileChunk`, chunk rotation, JFR file handling
73+
- `ProfileLifecycle` (MANUAL vs TRACE modes)
74+
- async-profiler integration, ServiceLoader discovery
75+
- Rate limiting, offline caching, scopes integration
76+
77+
- **Android profiling**: There is currently no dedicated rule for this area yet.
78+
- Inspect the relevant `sentry-android-core` profiling code directly
79+
- Fetch other related rules as needed (for example `options`, `offline`, or `api`)
80+
6981
### Integration & Infrastructure
7082
- **`opentelemetry`**: Use when working with:
7183
- OpenTelemetry modules (`sentry-opentelemetry-*`)
@@ -99,11 +111,13 @@ Use the `fetch_rules` tool to include these rules when working on specific areas
99111
- Public API/apiDump/.api files/binary compatibility/new method → `api`
100112
- Options/SentryOptions/ExternalOptions/ManifestMetadataReader/sentry.properties → `options`
101113
- Scope/Hub/forking → `scopes`
102-
- Duplicate/dedup → `deduplication`
114+
- Duplicate/dedup → `deduplication`
103115
- OpenTelemetry/tracing/spans → `opentelemetry`
104116
- new module/integration/sample → `new_module`
105117
- Cache/offline/network → `offline`
106118
- System test/e2e/sample → `e2e_tests`
107119
- Feature flag/addFeatureFlag/flag evaluation → `feature_flags`
108120
- Metrics/count/distribution/gauge → `metrics`
109121
- PR/pull request/stacked PR/stack → `pr`
122+
- JVM continuous profiling/async-profiler/JFR/ProfileChunk → `continuous_profiling_jvm`
123+
- Android continuous profiling/AndroidProfiler/frame metrics/method tracing → no dedicated rule yet; inspect the code directly

.cursor/rules/pr.mdc

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,8 @@ git push -u origin HEAD
185185
gh pr create --base main --draft --title "<type>(<scope>): <Topic>" --body "Collection PR for the <Topic> stack. Squash-merge this once all stack PRs are merged."
186186
```
187187

188+
**CRITICAL: Do NOT manually update the collection branch.** Never merge, fast-forward, or push stack branch commits into the collection branch. The collection branch stays at its initial position (the empty commit on `main`) until the user merges individual stack PRs into it one by one through GitHub. If you fast-forward the collection branch to include stack commits, GitHub will auto-merge and delete all stack PR branches, destroying the entire stack.
189+
188190
### Creating a New Stacked PR
189191

190192
1. Start from the tip of the previous stack branch (or the collection branch for the first PR).
@@ -197,35 +199,37 @@ gh pr create --base main --draft --title "<type>(<scope>): <Topic>" --body "Coll
197199

198200
### Stack List in PR Description
199201

200-
Every PR in the stack must have a stack list **at the top of its description** (before the `## :scroll: Description` section). When a new PR is added, update the description on **all** PRs in the stack.
202+
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 new PR is added, update the description on **all** PRs in the stack and on the collection branch PR.
201203

202204
Format:
203205

204206
```markdown
205207
## PR Stack (<Topic>)
206208

207-
- [#5118](https://github.com/getsentry/sentry-java/pull/5118) — Add scope-level attributes API
208-
- [#5120](https://github.com/getsentry/sentry-java/pull/5120) — Wire scope attributes into LoggerApi and MetricsApi
209-
- [#5121](https://github.com/getsentry/sentry-java/pull/5121) — Showcase scope attributes in Spring Boot 4 samples
209+
- #5118
210+
- #5120
211+
- #5121
210212

211213
---
212214
```
213215

214216
No status column — GitHub already shows that. The `---` separates the stack list from the rest of the PR description.
215217

216-
To update the PR description, use `--body-file` to avoid shell quoting issues with special characters in the body:
218+
**Merge method reminder:** On stack PRs (not the collection branch PR), add the following line at the very end of the PR description:
217219

218-
```bash
219-
# Get current PR description into a temp file
220-
gh pr view <PR_NUMBER> --json body --jq '.body' > /tmp/pr-body.md
220+
```markdown
221+
> ⚠️ **Merge this PR using a merge commit** (not squash). Only the collection branch is squash-merged into main.
222+
```
221223

222-
# Edit /tmp/pr-body.md to prepend or replace the stack list section
223-
# (replace everything from "## PR Stack" up to and including the "---" separator,
224-
# or prepend before the existing description if no stack list exists yet)
224+
This does not apply to standalone PRs or the collection branch PR.
225225

226-
# Update the description
227-
gh pr edit <PR_NUMBER> --body-file /tmp/pr-body.md
228-
```
226+
To update the PR description, use `--body-file` to avoid shell quoting issues with special characters in the body.
227+
228+
**Important:** Do not use shell redirects (`>`, `>>`, `|`) or compound commands (`&&`, `||`). These create compound shell expressions that won't match permission patterns. Instead, use the `Write` and `Edit` tools for file manipulation:
229+
230+
1. Read the current body with `gh pr view <PR_NUMBER> --json body --jq '.body'` (the output is returned directly — use the `Write` tool to save it to `/tmp/pr-body.md`)
231+
2. Use the `Edit` tool to prepend or replace the stack list section in `/tmp/pr-body.md`
232+
3. Update the description: `gh pr edit <PR_NUMBER> --body-file /tmp/pr-body.md`
229233

230234
### Merging Stacked PRs (done by the user, not the agent)
231235

@@ -237,12 +241,12 @@ Once all stack PRs are merged into the collection branch, the collection PR is *
237241

238242
### Syncing the Stack
239243

240-
When a base PR changes (e.g. after addressing review feedback on PR 1), merge the changes forward through the stack:
244+
When a base PR changes (e.g. after addressing review feedback on PR 1), merge the changes forward through the stack **between adjacent stack PR branches only**:
241245

242246
```bash
243247
# On the branch for PR 2
244248
git checkout feat/scope-attributes-logger
245-
git merge feat/scope-attributes
249+
git merge feat/scope-attributes-api
246250
git push
247251

248252
# On the branch for PR 3
@@ -251,4 +255,6 @@ git merge feat/scope-attributes-logger
251255
git push
252256
```
253257

258+
**Never merge into the collection branch.** Syncing only happens between stack PR branches. The collection branch is untouched until the user merges PRs through GitHub.
259+
254260
Prefer merge over rebase — it preserves commit history, doesn't invalidate existing review comments, and avoids the need for force-pushing. Only rebase if explicitly requested.

.github/workflows/agp-matrix.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ jobs:
6060

6161
- name: Create AVD and generate snapshot for caching
6262
if: steps.avd-cache.outputs.cache-hit != 'true'
63-
uses: reactivecircus/android-emulator-runner@b530d96654c385303d652368551fb075bc2f0b6b # pin@v2
63+
uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # pin@v2
6464
with:
6565
api-level: 30
6666
target: aosp_atd
@@ -79,7 +79,7 @@ jobs:
7979

8080
# We tried to use the cache action to cache gradle stuff, but it made tests slower and timeout
8181
- name: Run instrumentation tests
82-
uses: reactivecircus/android-emulator-runner@b530d96654c385303d652368551fb075bc2f0b6b # pin@v2
82+
uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # pin@v2
8383
with:
8484
api-level: 30
8585
target: aosp_atd

.github/workflows/changes-in-high-risk-code.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
- uses: actions/checkout@v6
2020
- name: Get changed files
2121
id: changes
22-
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
22+
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
2323
with:
2424
token: ${{ github.token }}
2525
filters: .github/file-filters.yml

.github/workflows/codeql-analysis.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
3737

3838
- name: Initialize CodeQL
39-
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # pin@v2
39+
uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # pin@v2
4040
with:
4141
languages: 'java'
4242

@@ -45,4 +45,4 @@ jobs:
4545
./gradlew buildForCodeQL --no-build-cache
4646
4747
- name: Perform CodeQL Analysis
48-
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # pin@v2
48+
uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # pin@v2

0 commit comments

Comments
 (0)