From 20065b694bff81c87f0ec7c6a8adb9d40674d7d7 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 16 Oct 2026 22:11:09 +0000 Subject: [PATCH 1/2] docs: Refresh docs against current code Co-Authored-By: Joshua Li <14209781+joshuarli@users.noreply.github.com> --- AGENTS.md | 27 ++++++++++++++++++++++----- README.md | 12 ++++++++---- docs/architecture.md | 4 +++- docs/options.md | 15 ++++++++------- docs/setup.md | 12 ++++++------ examples/README.md | 28 +++++++--------------------- 6 files changed, 54 insertions(+), 44 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index df4d4cf0..9960eec3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,10 +1,27 @@ # sentry-options -Sentry's internal option and feature-flag platform: schema-defined options, git-managed values, read at runtime — validated, hot-reloadable, and backed by Kubernetes ConfigMaps. +Sentry's internal option and feature-flag platform: schema-defined options, git-managed values, read at runtime. Values are validated, hot-reloadable, and backed by Kubernetes ConfigMaps. This repo holds the Rust and Python clients, the shared validation crate, `sentry-options-cli`, the `sentry-options-gen-stubs` pre-commit hook, and the reusable `validate-schema` workflow. + +## Commands + +- Rust format check: `cargo fmt --all -- --check` +- Rust lint: `cargo clippy --workspace --all-features --tests -- -D clippy::all` +- Rust tests: `SENTRY_OPTIONS_DIR=$PWD/sentry-options cargo test` (`.envrc` sets the variable) +- Python client tests, from `clients/python`: `uv sync --extra dev && uv run maturin develop && uv run pytest tests` +- Stub generator tests, from the repo root: `uv sync && uv run pytest` +- Pre-commit hooks: `pre-commit run --files ` + +## Invariants + +- Build the Python bindings (`clients/python`) with maturin, never `cargo build`. They are left out of the workspace `default-members` for this reason. +- Schema changes must follow the evolution rules in [docs/architecture.md](./docs/architecture.md#schema-evolution--validation). The `validate-schema` workflow enforces them. +- `CHANGELOG.md` is generated by craft at release (`changelogPolicy: auto` in `.craft.yml`). `scripts/bump-version.sh` sets the versions. ## Documentation -- [README](./README.md) — what it is and a code quickstart -- [docs/setup.md](./docs/setup.md) — integrating sentry-options into a new repo -- [docs/options.md](./docs/options.md) — adding/deleting options, setting values, testing -- [docs/architecture.md](./docs/architecture.md) — how it works: schema format, runtime model, feature-flag evaluation +- [README](./README.md): what it is and a code quickstart +- [docs/setup.md](./docs/setup.md): integrating sentry-options into a new repo +- [docs/options.md](./docs/options.md): adding and deleting options, setting values, testing +- [docs/architecture.md](./docs/architecture.md): schema format, runtime model, feature-flag evaluation, CLI output, the injector +- [examples/README.md](./examples/README.md): deploying the example image to a cluster +- [examples/minikube_propagation/README.md](./examples/minikube_propagation/README.md): measuring ConfigMap propagation latency locally diff --git a/README.md b/README.md index 2ddf9cf4..d0723ae2 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # sentry-options -Sentry's internal runtime option and feature-flag platform. Options are defined in your repo and values are set in `sentry-options-automator`. Multiple layers of validation, hot-reloaded, and minimal infrastructure. Awesome. +Sentry's internal runtime option and feature-flag platform. Options are defined in your repo and values are set in `sentry-options-automator`. Values are validated at several layers, hot-reloaded, and need minimal infrastructure. `sentry-options` was built to replace our legacy database-backed options with a straightforward config file (in the form of a mounted Kubernetes ConfigMap). This lowers infrastructure complexity, makes the code simpler to understand, and dramatically lowers upper percentile (i.e. P90) latency. `sentry-options` is also Rust native, supporting both Rust and Python client libraries out of the box. -## Documentation Reference +## Documentation | Doc | What it covers | | ----------------------------------------- | ------------------------------------------------------------------------- | @@ -12,9 +12,9 @@ Sentry's internal runtime option and feature-flag platform. Options are defined | [Working with options](./docs/options.md) | Adding/deleting options, using options, setting option values, testing | | [Architecture](./docs/architecture.md) | Technical reference: schema format, values & targets, runtime model, feature-flag evaluation | -## Quick Reference +## Quick reference -> See [Working with options](./docs/options.md) for a more detailed guide +To add sentry-options to a service, follow [Setup](./docs/setup.md). Then read options in code: ### Python @@ -45,3 +45,7 @@ fn main() -> anyhow::Result<()> { Ok(()) } ``` + +## Help and license + +Report bugs and ask questions in [GitHub issues](https://github.com/getsentry/sentry-options/issues). sentry-options is licensed under [Apache-2.0](./LICENSE). diff --git a/docs/architecture.md b/docs/architecture.md index 9c589fb1..c8fb46ea 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -196,6 +196,8 @@ Options must be initialized once at startup, guarded by a global `OnceLock`. Rus | `.with_callback(cb)` | Register a reload callback | | `.with_refresh_threshold(t)` | Override the refresh-on-read staleness threshold (default 5 s); `None` disables refresh-on-read | +Call `.build()` instead of `.init()` to get a standalone `Options` instance without setting the global store. The free functions `options()` and `features()` read only the global store. + `init()` is a shorthand for `Options::builder().init()`; the standalone `init_with_schemas` / `init_with_propagation_callback` functions are deprecated in favor of the builder. Python: `init(on_propagation=None, refresh_threshold=5.0, additional_schemas=None)`, where `additional_schemas` is a `{ns: json}` mapping equivalent to `.with_additional_schemas()`. The `init()` shorthand and Python's `init()` are idempotent — calling again is a no-op. The builder's `.init()` instead returns `OptionsError::AlreadyInitialized` when options are already initialized, so re-initializing with different settings is a loud error rather than a silent no-op. @@ -216,7 +218,7 @@ End to end, a value change propagates as: ConfigMap update → ~1–2 min kubele Every refresh — lazy or manual — resets the staleness timer. Two consequences: - **Preempting hot-path IO**: driving `refresh()` from your own timer more often than the threshold guarantees that reads never stat or parse files inline. -- **Manual-only mode**: passing `None` as the refresh threshold disables refresh-on-read entirely; values then only change when `refresh()` is called. Combined with the reload callback, this supports eagerly re-deserializing options into a typed struct off the hot path whenever `refresh()` reports a change. +- **Manual-only mode**: passing `None` as the refresh threshold disables refresh-on-read entirely; values then only change when `refresh()` is called. In Rust, `refresh()` is a method on an `Options` instance from `.build()`. The global store from `.init()` has no public `refresh()`. Combined with the reload callback, this supports eagerly re-deserializing options into a typed struct off the hot path whenever `refresh()` reports a change. ### Propagation metric diff --git a/docs/options.md b/docs/options.md index b73f95cc..913e2161 100644 --- a/docs/options.md +++ b/docs/options.md @@ -12,6 +12,7 @@ The first half of this document covers runtime options. The second half covers f - [Setting an option value locally](#setting-an-option-value-locally) - [Adding an option](#adding-an-option) - [Setting an option value](#setting-an-option-value) + - [Diffing effective values](#diffing-effective-values) - [Deleting an option](#deleting-an-option) - [Reading a feature flag](#reading-a-feature-flag) - [Definitions](#definitions) @@ -117,10 +118,10 @@ use sentry_options::{options}; // call init() somewhere early #[test] fn test_timeout() { - assert_eq!(options("seer").get("autofix.enabled").unwrap(), false); + assert_eq!(options("seer").unwrap().get("autofix.enabled").unwrap(), false); // will be `true` for the lifetime of this guard let _guard = override_options(&[("seer", "autofix.enabled", true.into())]).unwrap(); - assert_eq!(options("seer").get("autofix.enabled").unwrap(), true); + assert_eq!(options("seer").unwrap().get("autofix.enabled").unwrap(), true); } ``` @@ -129,9 +130,9 @@ Just like in Python, use `get_forced()` to bypass the 5s cache threshold and see // import, create a values file, init(), etc... fs::write("sentry-options/values/seer/values.json", br#"{"options": {"autofix.enabled": false}}"#)?; // Cached get() still returns the old value within the 5s threshold. -assert_eq!(options("seer").get("autofix.enabled").unwrap(), true); +assert_eq!(options("seer")?.get("autofix.enabled")?, true); // get_forced() bypasses the cache and returns the updated value immediately. -assert_eq!(options("seer").get_forced("autofix.enabled").unwrap(), false); +assert_eq!(options("seer")?.get_forced("autofix.enabled")?, false); ``` ## Setting an option value locally @@ -230,7 +231,7 @@ Dicts can have arbitrary keys that match the value type, so be careful when doin {"anything": "x", "goes": "y"} // arbitrary keys, all string values ``` -Adding an option with the Python library installed should trigger the ([precommit hook](./setup.md#2-add-workflows)). +If you installed the [`sentry-options-gen-stubs` pre-commit hook](./setup.md#2-add-workflows), it regenerates the Python type stubs when you change the schema. ## Setting an option value @@ -501,9 +502,9 @@ options: Similar to options, deleting a feature flag requires removing 3 components: 1. The usage in code (the `features.has(...)` callsite) -2. The definition (`manager.add` in sentry's `temporary.py` or `permanent.py`, or getsentry's `features.py`) +2. The definition: the `feature.` entry in `sentry-options/schemas/{namespace}/schema.json`. For sentry and getsentry, it is the `manager.add` call in sentry's `temporary.py` or `permanent.py`, or getsentry's `features.py`. 3. The value (in `sentry-options-automator`) The above ordering is recommended to prevent any regressions in behavior. **1.** and **2.** can be done simultaneously in the same PR. Once the definition is removed the value is no longer read, but nothing flags a leftover `flagpole.yaml` block, so please remove the value yourself. -If you remove the value (**3.**) first, `features.has()` falls back to the `default=` in `manager.add`, usually `False`, so the flag turns off for everyone. If you remove the definition (**2.**) while checks remain, `features.has()` returns `False` and reports the error to Sentry. +If you remove the value (**3.**) first, the flag turns off for everyone. The sentry-options client returns `False` for a flag with no value, and sentry and getsentry fall back to the `default=` in `manager.add`, usually `False`. If you remove the definition (**2.**) while checks remain, `has()` returns `False`. sentry and getsentry also report the error to Sentry. diff --git a/docs/setup.md b/docs/setup.md index bed33193..d59b1412 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -75,7 +75,7 @@ jobs: validate: needs: cli-version - uses: getsentry/sentry-options/.github/workflows/validate-schema.yml@0b115be89b102d76beff8106bd4054365954282e + uses: getsentry/sentry-options/.github/workflows/validate-schema.yml@1a5e3a280d665ceae23d3c98bc3cd56e6d94ab0f # 1.2.17 secrets: SENTRY_READONLYBOT_PRIVATE_KEY: ${{ secrets.SENTRY_READONLYBOT_PRIVATE_KEY }} with: @@ -90,7 +90,7 @@ In `.pre-commit-config.yaml` ```yaml # Generates sentry-options type stubs - repo: https://github.com/getsentry/sentry-options - rev: 0b115be89b102d76beff8106bd4054365954282e + rev: 1a5e3a280d665ceae23d3c98bc3cd56e6d94ab0f # 1.2.17 hooks: - id: sentry-options-gen-stubs ``` @@ -102,14 +102,14 @@ Again, check the repo for the latest SHAs (pinning actions at SHAs is better pra In Python, this would be in `pyproject.toml` or similar: ```toml -dependencies = ["sentry-options>=1.1.1"] +dependencies = ["sentry-options>=1.2.17"] ``` In Rust, this would be in `Cargo.toml`: ```toml [dependencies] -sentry-options = "1.1.1" +sentry-options = "1.2.17" ``` Check the [releases page](https://github.com/getsentry/sentry-options/releases) for the latest version. @@ -128,7 +128,7 @@ init() # optional: init(on_propagation=Callback, refresh_threshold=5.0) `on_propagation` is a callback fired whenever values reload, useful for collecting metrics. `refresh_threshold` sets the staleness window for refresh-on-read (default `5.0` seconds). -**Important:** Passing `None` into `refresh_threshold` will completely disable automatic refreshes and must be triggered manually via `sentry_options.refresh`. +**Important:** Passing `None` to `refresh_threshold` disables automatic refreshes. Values then change only when you call `sentry_options.refresh()`. #### Rust @@ -152,7 +152,7 @@ Options::builder() .init()?; ``` -**Important:** Passing `None` into `refresh_threshold` will completely disable automatic refreshes and must be triggered manually via `sentry_options.refresh`. +**Important:** Passing `None` to `with_refresh_threshold` disables automatic refreshes. Values then change only when you call `refresh()` on an `Options` instance, which you get from `Options::builder().build()` instead of `.init()`. The global store that `.init()` creates has no public `refresh()`, so `.with_refresh_threshold(None).init()` never reloads values. `.init()` returns `OptionsError::AlreadyInitialized` if options are already initialized; ignore it with `.ok()` if that's expected. The standalone `init_with_schemas()` and `init_with_propagation_callback()` functions are **deprecated** in favor of the builder. diff --git a/examples/README.md b/examples/README.md index 612d4be0..dbc7c07a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -8,7 +8,7 @@ The deployment model: - **Schemas** are baked into the Docker image at build time - **Values** are mounted via Kubernetes ConfigMap at runtime -- The client watches for ConfigMap changes and hot-reloads values without pod restart +- The client re-reads the mounted values on read and picks up ConfigMap changes without a pod restart ## Prerequisites @@ -30,14 +30,6 @@ docker tag sentry-options-example \ docker push REGION-docker.pkg.dev/PROJECT_ID/REPO/sentry-options-test:latest ``` -Example for sandbox `sbx-hdeng-1`: - -```bash -docker tag sentry-options-example \ - us-west1-docker.pkg.dev/eng-dev-sbx--hdeng-2/hdeng-images/sentry-options-test:latest -docker push us-west1-docker.pkg.dev/eng-dev-sbx--hdeng-2/hdeng-images/sentry-options-test:latest -``` - ## 2. Generate and Apply ConfigMap ```bash @@ -67,13 +59,7 @@ kubectl apply -f configmap.yaml ## 3. Deploy Example Pod -An example deployment exists in `terraform-sandboxes.private`: - -``` -terraform-sandboxes.private/personal-sandbox/env/sbx-hdeng-1/k8s/deployment.yaml -``` - -The deployment mounts the ConfigMap as a volume: +Save a deployment that mounts the ConfigMap as a volume to `deployment.yaml`: ```yaml apiVersion: apps/v1 @@ -92,7 +78,7 @@ spec: spec: containers: - name: test - image: us-west1-docker.pkg.dev/eng-dev-sbx--hdeng-2/hdeng-images/sentry-options-test:latest + image: REGION-docker.pkg.dev/PROJECT_ID/REPO/sentry-options-test:latest volumeMounts: - name: values mountPath: /etc/sentry-options/values/sentry-options-testing @@ -114,14 +100,14 @@ kubectl apply -f deployment.yaml Watch the pod logs: ```bash -kubectl logs -f deployment/sentry-options-example +kubectl logs -f deployment/sentry-options-test ``` You should see output like: ``` -values: UPDATED-VALUE | 0.99 | False -values: UPDATED-VALUE | 0.99 | False +values: UPDATED-VALUE | 0.99 | False | default +values: UPDATED-VALUE | 0.99 | False | default ``` Update a value and re-apply: @@ -140,7 +126,7 @@ Update a value and re-apply: | kubectl apply -f - ``` -The pod will pick up the new values within ~5 seconds without restarting. +The pod picks up the new values without restarting. Expect a delay of up to the kubelet sync period (about 1 to 2 minutes) plus the 5 second refresh threshold. ## Directory Structure From 1fa910ed9fe7a52ac27cadce29900dcfb625396c Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Fri, 16 Oct 2026 22:14:58 +0000 Subject: [PATCH 2/2] docs(setup): Point repos.json path at the schemas directory Co-Authored-By: Joshua Li <14209781+joshuarli@users.noreply.github.com> --- docs/setup.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/setup.md b/docs/setup.md index d59b1412..cb8292cf 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -186,7 +186,7 @@ For validation to succeed in `sentry-options-automator` it must first know your "repos": { "seer": { "url": "https://github.com/getsentry/seer", - "path": "sentry-options/" + "path": "sentry-options/schemas" } } }