diff --git a/.cursor/rules/api.mdc b/.cursor/rules/api.mdc new file mode 100644 index 0000000000..c5a793d924 --- /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/options.mdc b/.cursor/rules/options.mdc new file mode 100644 index 0000000000..2d239da781 --- /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 index f05d2992d4..a982cfe960 100644 --- a/.cursor/rules/overview_dev.mdc +++ b/.cursor/rules/overview_dev.mdc @@ -15,6 +15,19 @@ These rules are automatically included in every conversation: Use the `fetch_rules` tool to include these rules when working on specific areas: ### Core SDK Functionality +- **`api`**: Use when working with: + - Adding or modifying public API surface + - Binary compatibility, `.api` files, `apiDump` + - Understanding which classes to modify for new API (interfaces, implementations, no-ops, adapters) + - `IScope`, `IScopes`, `Sentry` static API + - Attributes, logging API, protocol classes + +- **`options`**: Use when working with: + - Adding or modifying SDK options (`SentryOptions`, namespaced options) + - External options (`ExternalOptions`, `sentry.properties`, environment variables) + - Android manifest metadata (`ManifestMetadataReader`) + - Spring Boot properties (`SentryProperties`) + - **`scopes`**: Use when working with: - Hub/Scope management, forking, or lifecycle - `Sentry.getCurrentScopes()`, `pushScope()`, `withScope()` @@ -63,6 +76,13 @@ Use the `fetch_rules` tool to include these rules when working on specific areas - **`new_module`**: Use when adding a new integration or sample module +### Workflow +- **`pr`**: Use when working with: + - Creating pull requests + - Stacked PRs, PR naming, stack comments + - PR changelog entries + - Merging or syncing stacked branches + ### Testing - **`e2e_tests`**: Use when working with: - System tests, sample applications @@ -76,6 +96,8 @@ Use the `fetch_rules` tool to include these rules when working on specific areas 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: + - Public API/apiDump/.api files/binary compatibility/new method → `api` + - Options/SentryOptions/ExternalOptions/ManifestMetadataReader/sentry.properties → `options` - Scope/Hub/forking → `scopes` - Duplicate/dedup → `deduplication` - OpenTelemetry/tracing/spans → `opentelemetry` @@ -84,3 +106,4 @@ Use the `fetch_rules` tool to include these rules when working on specific areas - System test/e2e/sample → `e2e_tests` - Feature flag/addFeatureFlag/flag evaluation → `feature_flags` - Metrics/count/distribution/gauge → `metrics` + - PR/pull request/stacked PR/stack → `pr` diff --git a/.cursor/rules/pr.mdc b/.cursor/rules/pr.mdc new file mode 100644 index 0000000000..581c8cc95d --- /dev/null +++ b/.cursor/rules/pr.mdc @@ -0,0 +1,231 @@ +--- +alwaysApply: false +description: Pull request creation, stacked PRs, and PR workflow +--- + +# Pull Request Rules + +## Creating a Pull Request + +### Step 1: Ensure Feature Branch + +If on `main`, create and switch to a new branch: + +```bash +git checkout -b / +``` + +Branch names use `feat/`, `fix/`, `ref/`, etc. matching the commit type. + +### Step 2: Format Code and Regenerate API Files + +```bash +./gradlew spotlessApply apiDump +``` + +This is **required** before every PR. Fix any failures before continuing. + +### Step 3: Commit Changes + +Use `git status --porcelain` to review changes. Ignore files only relevant for local testing (hardcoded debug toggles, sample app config, `.env` files). Restore those with `git checkout -- `. + +Follow [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` + +- Use imperative present tense ("add" not "added") +- Capitalize subject, no trailing period +- Keep under 100 characters + +### Step 4: Push + +```bash +git push -u origin HEAD +``` + +If push fails due to diverged history, ask the user — do not force-push. + +### Step 5: Create PR + +Create a draft PR using the repo's PR template: + +```markdown +## :scroll: Description + + +## :bulb: Motivation and Context + + +## :green_heart: How did you test it? + + +## :pencil: Checklist +- [ ] I added GH Issue ID _&_ Linear ID +- [ ] I added tests to verify the changes. +- [ ] No new PII added or SDK only sends newly added PII if `sendDefaultPII` is enabled. +- [ ] I updated the docs if needed. +- [ ] I updated the wizard if needed. +- [ ] 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. + +## :crystal_ball: Next steps +``` + +### Step 6: Update Changelog + +Add an entry to `CHANGELOG.md` under `## Unreleased` in the appropriate subsection: + +| Change Type | Subsection | +|---|---| +| New feature | `### Features` | +| Bug fix | `### Fixes` | +| Refactoring, internal cleanup | `### Internal` | +| Dependency update | `### Dependencies` | + +Entry format: + +```markdown +- ([#](https://github.com/getsentry/sentry-java/pull/)) +``` + +Commit changelog separately: + +```bash +git add CHANGELOG.md && git commit -m "changelog" && git push +``` + +### PR Title Format + +Follow the commit message format: + +``` +(): +``` + +Examples: +- `feat(core): Add structured logging support` +- `fix(android): Prevent crash on API 21 when registering receiver` + +--- + +## Stacked PRs + +Stacked PRs split a large feature into small, easy-to-review PRs where each builds on the previous one. This follows the same concept as the [Graphite](https://graphite.dev/) stacking workflow. + +### Structure + +``` +main → stack-pr-1 → stack-pr-2 → stack-pr-3 → ... +``` + +- The first PR in the stack targets `main` as its base branch. +- Each subsequent PR targets the previous stack PR's branch as its base. +- Each PR contains only incremental changes on top of the previous one. + +### Branch Naming + +Prefer a shared prefix for the feature, with descriptive suffixes per PR. The type prefix (`feat/`, `fix/`, etc.) may vary depending on the nature of each PR's changes: + +``` +feat/scope-attributes # PR 1 +feat/scope-attributes-logger # PR 2 +fix/attribute-type-detection # PR 3 (fix, different name — that's fine) +``` + +### 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` +- `feat(samples): [Global Attributes 3] Showcase scope attributes in Spring Boot 4 sample` + +### Finding All PRs in a Stack + +Do **not** rely on branch name patterns — later PRs in a stack may use different prefixes or naming. Instead: + +1. Find the PR for the current branch: + ```bash + gh pr list --head "$(git branch --show-current)" --json number,title,baseRefName --jq '.[0]' + ``` +2. Read the stack comment on that PR — it lists every PR in the stack. +3. If there is no stack comment yet, walk the chain in both directions: + ```bash + # Find the PR whose head branch is the current PR's base (go up) + gh pr list --head --json number,title,baseRefName + + # Find PRs whose base branch is the current PR's head (go down) + gh pr list --base --json number,title,headRefName + ``` + Repeat until you reach `main` going up and find no more PRs going down. + +### Creating a New Stacked PR + +1. Start from the tip of the previous stack branch (or `main` for the first PR). +2. Create a new branch, make changes, format, commit, and push. +3. Create the PR with `--base `: + ```bash + gh pr create --base feat/previous-branch --draft --title "(): [ ] " --body "..." + ``` +4. Add the stack comment to the new PR and update it on all existing PRs in the stack (see below). + +### Stack Comment + +Every PR in the stack must have a comment listing all PRs in the stack. When a new PR is added, update the comment on **all** PRs in the stack. + +Format: + +```markdown +## PR Stack () + +- [#5118](https://github.com/getsentry/sentry-java/pull/5118) — Add scope-level attributes API +- [#5120](https://github.com/getsentry/sentry-java/pull/5120) — Wire scope attributes into LoggerApi and MetricsApi +- [#5121](https://github.com/getsentry/sentry-java/pull/5121) — Showcase scope attributes in Spring Boot 4 samples +``` + +No status column — GitHub already shows that. + +To add or update the stack comment on a PR: + +```bash +# Find existing stack comment (if any) +gh api repos/getsentry/sentry-java/issues//comments --jq '.[] | select(.body | startswith("## PR Stack")) | .id' + +# Create new comment +gh pr comment --body "" + +# Or update existing comment +gh api repos/getsentry/sentry-java/issues/comments/ -X PATCH -f body="" +``` + +### Merging Stacked PRs + +Merge in order from bottom to top (PR 1 first, then PR 2, etc.). After each merge, the next PR's base automatically becomes the merged branch's target. GitHub handles rebasing onto the new base. Verify each PR's diff still looks correct after the previous one merges. + +### Syncing the Stack + +When a base PR changes (e.g. after addressing review feedback on PR 1), merge the changes forward through the stack: + +```bash +# On the branch for PR 2 +git checkout feat/scope-attributes-logger +git merge feat/scope-attributes +git push + +# On the branch for PR 3 +git checkout feat/scope-attributes-sample +git merge feat/scope-attributes-logger +git push +``` + +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.