Repository navigation
feat(cli): Make SDK calls write no local state by default - #2074
Conversation
createSentrySDK({ persist: false }) runs each invocation without reading or
writing local state: the database is a null store, and the response cache,
.sentryclirc files, custom CA files, version checks, and telemetry are off.
A new env option sets the base environment instead of process.env.
Closes #1897
Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
createSentrySDK() now writes no local state unless created with persist: true. Calls still read cli.db through a read-only view (stored logins, defaults, cached lookups) and .sentryclirc files, and telemetry no longer depends on persist. Co-Authored-By: BYK <126780+BYK@users.noreply.github.com>
findProjectRoot cached the walk-local .sentryclirc config without the global fallbacks when it found no DSN or VCS root, so a later loadSentryCliRc for the same cwd skipped ~/.sentryclirc and $SENTRY_CONFIG_DIR/.sentryclirc.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit acf5a68. Configure here.
When a persist: false call couldn't open cli.db, getSharedDatabase still installed an in-memory database as the process-wide connection, so a later persist: true call reused it without retrying the file or warning. Read-only callers now get the error and nothing is installed.
| const nullStatement: Statement = { | ||
| get: () => null, | ||
| all: () => [], | ||
| run: () => { | ||
| // Writes are dropped. | ||
| }, | ||
| }; | ||
|
|
||
| /** The shared null store. It holds no state, so one instance serves every call. */ | ||
| export const nullDatabase: DatabaseHandle = { | ||
| exec: () => { | ||
| // Statements are dropped. | ||
| }, | ||
| query: () => nullStatement, | ||
| transaction: | ||
| <T>(fn: () => T) => | ||
| () => | ||
| fn(), | ||
| close: () => { | ||
| // Nothing to close. | ||
| }, | ||
| }; | ||
|
|
||
| const READ_STATEMENT = /^\s*SELECT\b/i; | ||
|
|
||
| /** | ||
| * A view of `db` that reads but never writes. `close()` does nothing: the | ||
| * connection is shared with invocations that persist. | ||
| */ | ||
| export function readOnlyView(db: DatabaseHandle): DatabaseHandle { | ||
| return { | ||
| ...nullDatabase, | ||
| query: (sql) => (READ_STATEMENT.test(sql) ? db.query(sql) : nullStatement), | ||
| }; |
There was a problem hiding this comment.
Read-only run() makes OAuth refresh think credentials were saved
Dropped writes still succeed, so persistRefreshedCredential returns true after a no-op UPDATE, clears auth caches, and leaves the old tokens in cli.db. With refresh-token rotation (already covered by auth tests), the next refresh in the same or later SDK call uses the stale refresh token and auth fails.
Evidence
nullStatement.run()is a silent no-op, andreadOnlyView()routes non-SELECTSQL (includingUPDATE accounts SET token/refresh_token...) to that statement.persistRefreshedCredential()inauth.tsruns that UPDATE then returnstrueand resets auth caches with no row-change check.performTokenRefresh()treats thattrueas success and returns the new tokens only in-memory;cli.dbstill holds the pre-refresh row.- Auth concurrency tests model per-refresh
refresh_tokenrotation; a laterrefreshToken()re-reads the stale refresh token from DB and fails once the server has rotated it. - Default
createSentrySDK()setsSENTRY_CLI_NO_PERSISTENCE, so stored OAuth logins hit this path withoutpersist: true.
Also found at 3 additional locations
packages/cli/src/lib/sdk-types.ts:69-77packages/cli/src/lib/db/index.ts:21-21packages/cli/test/lib/sdk-no-persistence.test.ts:217-229
Identified by Warden · code-review · PQJ-QG5
**Why:** Before `sentry` 0.49.0, every `createSentrySDK()` call opened and wrote the CLI's local `cli.db`. When a build runs on a different Node version than the one that created that file (for example through `npx sentry`), the open fails with `SQLite3Error: unable to open database file`. The build still passes, but release creation and source map uploads fail silently. In 0.49.0, SDK calls write no local state by default (getsentry/toolkit#2074) and fall back to running without `cli.db` (getsentry/toolkit#2069, getsentry/toolkit#2070). This bumps `@sentry/bundler-plugins`, `@sentry/remix` and `@sentry/react-router`, plus the bundler plugin integration tests so the workspace resolves a single version. None of them set `persist`, so the new default applies as-is. **How to review:** The four `package.json` ranges and the lockfile. 0.49.0 was published today, so pnpm added it to `minimumReleaseAgeExclude`, as with the earlier first-party `@sentry/*` entries there. **Unsure about:** The `cloudflare-*-send-to-sentry` e2e apps pin `sentry@~0.44.1` on their own. I left them alone because they're separate test apps and don't go through the bundler plugin. Closes #26197 <!-- junior-request-attribution:start --> via @BYK. <!-- junior-request-attribution:end --> <!-- junior-session-footer:start --> <!-- junior-conversation-id:local%3Aweb%3A87622ce3671938950bc6e630 --> -- [View Session in Sentry](https://sentry.sentry.io/explore/conversations/local%3Aweb%3A87622ce3671938950bc6e630/?project=4510944073809921) <!-- junior-session-footer:end --> Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com> Co-authored-by: BYK <126780+BYK@users.noreply.github.com>

Closes #1897.
createSentrySDK()calls now write no local state by default. They still read what the CLI stored, so stored logins, defaults and cached lookups incli.dbwork, and so do.sentryclircfiles and the custom CA file. They never create or changecli.db, and the response cache and version check are off. Passpersist: trueto get the old behavior, where SDK calls write state like the CLI.How it works:
SENTRY_CLI_NO_PERSISTENCEflag in the per-call env thatwithEnvalready isolates, so the setting applies to that call only.getDatabase()returns a read-only view of the shared connection that only runsSELECTstatements. Everything else is dropped, including theDELETE ... RETURNINGdrains (telemetry queue, completion queue) that read throughget()/all().cli.db, or it won't open (the WAL/driver cases from fix(cli): Read a WAL-mode database on the WASM SQLite driver #2069 and fix(cli): Use an in-memory database when cli.db won't open #2070, or a runtime with no SQLite driver), calls get a null store and no warning. The null store skips schema setup entirely.persist. It keeps its own opt-outs (SENTRY_CLI_NO_TELEMETRY,DO_NOT_TRACK).A new
envoption sets the environment each call starts from (defaultprocess.env). On a server, passenvwith only what the call needs, so the server's ownSENTRY_DSNdoesn't change target resolution.Behavior changes without
persist: true:cursor: "next"/"prev", since the cursor stack isn't written. Pass the returnednextCursorinstead."first"and raw cursors work. The e2e test that pages withcursor: "next"now passespersist: true.getRawDatabase()throws, socli fixandcli upgradeneedpersist: true.cli.dbstill runs the schema setup and migrations any CLI run does, which can write on an older schema.Also in here:
.sentryclirccaches are keyed by the activeSENTRY_CONFIG_DIR, sinceenvcan change it per call.This is a breaking change for SDK users, but the package is still
0.x. The bundler plugins pinsentry@^0.45, so they pick up the new default only when they bump on purpose. That bump is the fix for the plugins'cli.dbfailures. Module-level state shared between calls in one process (the shared connection, the Sentry client, the custom-header and region caches) is left to #1898.via @BYK.
--
View Session in Sentry