Skip to content

feat(options): Auto-generate sentry-options schema from Rust types#426

Draft
jan-auer wants to merge 15 commits into
mainfrom
feat/options-schema-gen
Draft

feat(options): Auto-generate sentry-options schema from Rust types#426
jan-auer wants to merge 15 commits into
mainfrom
feat/options-schema-gen

Conversation

@jan-auer

@jan-auer jan-auer commented Apr 10, 2026

Copy link
Copy Markdown
Member

Note

This is a proof-of-concept showcase, not intended for merge here. The implementation belongs in the sentry-options repo once #425 has landed there. This PR exists to demonstrate the approach and invite feedback before that upstream work begins. It has been written with Claude Code and the code has not been cleaned up or improved yet.

Stacked on #425.


The sentry-options/schemas/objectstore/schema.json was hand-written and had already drifted out of sync with the Rust types. This introduces auto-generation so the schema is always derived from the source of truth.

Approach

schemars derives JsonSchema on the options struct and its nested types (test-only, so schemars stays out of the production binary). A conversion function in objectstore-typed-options::schema translates the schemars output into sentry-options format, applying the per-level rules that format enforces — description and default only at top-level properties, optional only in nested item properties, no description or default deeper in the tree.

The schema file remains checked in for two reasons: sentry-options-automator needs access to it without running Rust, and reviewers can see schema changes directly in PRs without running anything locally.

Drift detection

A thin golden-file test in objectstore-options calls assert_schema_matches_golden_file::<Options>() from the shared library. When the schema is out of date, the test fails with a colored unified diff so the delta is immediately readable. To regenerate:

UPDATE_SCHEMA=1 cargo test -p objectstore-options

Adding or renaming a field, changing a doc comment, or adjusting optionality will all cause the test to fail with a precise diff pointing at exactly what changed.

What lives where

  • objectstore-typed-options::schema — reusable conversion and assertion logic; activated by the testing feature
  • objectstore-options — one test, one path construction; everything else is in the library

jan-auer added 12 commits April 7, 2026 17:57
…ry-options support

Introduces the `objectstore-options` crate, which wraps `sentry-options` and exposes
Objectstore-specific runtime configuration. Includes:

- `Options` struct with a global `OnceLock<RwLock<Options>>` snapshot refreshed every 5s
- `init()` function that loads an initial snapshot and spawns a background refresh task
- `Killswitch` type (plain data, deserialized from options) with usecase, scopes, and service fields
- Test-mode `Options::get()` that deserializes fresh from a thread-local instance, enabling
  `sentry_options::testing::override_options` to work without calling `init()`
- Objectstore killswitches JSON schema added to `sentry-options/schemas/objectstore/`
- `objectstore_options::init(None)` wired into the server CLI startup path
- Wire objectstore-options into objectstore-server killswitches, replacing
  the static config-backed implementation with live-reloading via sentry-options
- Switch objectstore-options global to OnceLock<ArcSwap<Options>> for lock-free reads
- Patch sentry-options to feat/additional-properties-map-schema branch to support
  object-typed schema fields with additionalProperties
…stale comment

Only spawn the background refresh task when OPTIONS.set() succeeds, so
concurrent callers cannot produce multiple refresh tasks and filesystem
watchers. Also remove a low-value inline comment in killswitches.rs.
* origin/main: (99 commits)
  fix(server): Ignore client-supplied read-only metadata on writes (#535)
  fix(stresstest): Use `put_path` for `many` requests (#533)
  fix(server): Strip Kubernetes hashes from downstream service (#530)
  ci: Scope sentry-options secrets instead of inherit (#534)
  build(client): Remove version pin notice on tokio dependency (#531)
  ref(server): Capture errors from backend servers in Sentry (#525)
  fix(server): Remove service tag from request metric (#528)
  fix(sentry): Bind hub to response bodies via middleware (#526)
  build: Disable in-process symbolication for heap profiles (#527)
  fix(dev): Only pass profiling feature for objectstore-server (#523)
  feat(server): Add on-demand heap profiling endpoints (#522)
  feat: Add filename field to Metadata (#517)
  docs: Fix and expand documentation across the workspace (#521)
  fix: Replace lossy casts with safer integer idioms (#518)
  build(deps): bump sentry to 0.48.3 (#520)
  ref(server): Skip auth checks when not configured (#519)
  feat(client): Accept optional tokens in ClientBuilder::token (#516)
  fix: Remove needless return statements in tiered backend tests (#514)
  fix: Use jemalloc only on linux (#515)
  fix: Apply pedantic Clippy lints across workspace (#513)
  ...

# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	README.md
#	objectstore-server/Cargo.toml
#	objectstore-server/src/extractors/id.rs
#	objectstore-server/src/killswitches.rs
#	objectstore-server/tests/limits.rs
Replace the tokio-spawned polling loop with a propagation callback
registered via InitBuilder, and switch to the feat/expose-builder-build
branch of sentry-options. Bump arc-swap and lru accordingly.

WIP: refresh() still references the built `inner` instance, which is no
longer in scope — the callback needs a path to reach it. Does not compile.
Introduces two new crates that replace the hand-written boilerplate in
objectstore-options:

- `objectstore-typed-options` — defines the `SentryOptions` trait, the
  shared `Error` type, and the generic background `refresh` loop. All
  dependencies consumed by generated code (arc-swap, serde, serde_json,
  sentry-options, tokio) are re-exported as hidden items so consumers
  do not need to declare them directly. Exposes a `derive` feature that
  re-exports the proc macro.

- `objectstore-typed-options-derive` — provides `#[derive(SentryOptions)]`,
  which generates the global OnceLock singleton, trait impl, `get()`,
  `init()`, and (under the `testing` feature) `override_with()`. All
  generated paths are fully qualified through `objectstore_typed_options`,
  so the derive can be used from any crate.

`objectstore-options` is reduced to the concrete struct definitions:

- Its dependency list shrinks to `objectstore-typed-options` (with the
  `derive` feature) and `serde`.
- The `init()` free function is gone; callers use `Options::init()`.
Two structs deriving SentryOptions in the same module previously produced
a duplicate symbol error because __OPTIONS was emitted at module scope with
a fixed name. Wrapping all generated items in const _: () = { … } gives
each derived struct its own anonymous scope, matching the standard
serde-style hygiene idiom.
@jan-auer
jan-auer force-pushed the feat/sentry-options-derive branch from d48e54d to db06791 Compare July 6, 2026 08:37
jan-auer and others added 3 commits July 6, 2026 11:01
Add schema generation support to objectstore-typed-options so the
sentry-options schema stays in sync with the Rust struct definitions.
Previously the schema was hand-written and had already drifted.

The conversion logic lives in objectstore_typed_options::schema and is
gated behind the testing feature. Consumers add JsonSchema derives (also
test-only) and call assert_schema_matches_golden_file in a single test.

The schema remains checked in so sentry-options-automator can access it
without running Rust, and so reviewers can inspect schema changes in
PRs. The golden file test catches drift immediately and prints a colored
unified diff; UPDATE_SCHEMA=1 regenerates it.

Co-Authored-By: Claude <noreply@anthropic.com>
@jan-auer
jan-auer force-pushed the feat/options-schema-gen branch from db98af4 to 3c84c44 Compare July 6, 2026 10:38
@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.76712% with 31 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.08%. Comparing base (8495f53) to head (3c84c44).

Files with missing lines Patch % Lines
objectstore-typed-options/src/schema.rs 77.85% 31 Missing ⚠️
Additional details and impacted files
@@                      Coverage Diff                       @@
##           feat/sentry-options-derive     #426      +/-   ##
==============================================================
- Coverage                       87.33%   87.08%   -0.25%     
==============================================================
  Files                              92       93       +1     
  Lines                           14534    14564      +30     
==============================================================
- Hits                            12693    12683      -10     
- Misses                           1841     1881      +40     
Components Coverage Δ
Rust Backend 91.99% <ø> (-0.16%) ⬇️
Rust Client 79.89% <ø> (ø)
Python Client 88.70% <ø> (-0.17%) ⬇️

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jan-auer
jan-auer force-pushed the feat/sentry-options-derive branch 3 times, most recently from 29d2057 to 46ce99d Compare July 8, 2026 15:59
Base automatically changed from feat/sentry-options-derive to main July 9, 2026 07:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant