Skip to content

feat(cli): Make SDK calls write no local state by default - #2074

Merged
BYK merged 6 commits into
mainfrom
feat/cli-no-persistence
Nov 7, 2026
Merged

BYK merged 6 commits into
mainfrom
feat/cli-no-persistence

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

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 in cli.db work, and so do .sentryclirc files and the custom CA file. They never create or change cli.db, and the response cache and version check are off. Pass persist: true to get the old behavior, where SDK calls write state like the CLI.

How it works:

  • The SDK sets an internal SENTRY_CLI_NO_PERSISTENCE flag in the per-call env that withEnv already isolates, so the setting applies to that call only.
  • With the flag set, getDatabase() returns a read-only view of the shared connection that only runs SELECT statements. Everything else is dropped, including the DELETE ... RETURNING drains (telemetry queue, completion queue) that read through get()/all().
  • When there's no 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.
  • Telemetry no longer depends on persist. It keeps its own opt-outs (SENTRY_CLI_NO_TELEMETRY, DO_NOT_TRACK).

A new env option sets the environment each call starts from (default process.env). On a server, pass env with only what the call needs, so the server's own SENTRY_DSN doesn't change target resolution.

Behavior changes without persist: true:

  • List commands reject cursor: "next"/"prev", since the cursor stack isn't written. Pass the returned nextCursor instead. "first" and raw cursors work. The e2e test that pages with cursor: "next" now passes persist: true.
  • getRawDatabase() throws, so cli fix and cli upgrade need persist: true.
  • Opening an existing cli.db still runs the schema setup and migrations any CLI run does, which can write on an older schema.

Also in here:

  • The .sentryclirc caches are keyed by the active SENTRY_CONFIG_DIR, since env can change it per call.
  • The Spleen font loads on first use instead of at import.

This is a breaking change for SDK users, but the package is still 0.x. The bundler plugins pin sentry@^0.45, so they pick up the new default only when they bump on purpose. That bump is the fix for the plugins' cli.db failures. 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

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>
@vercel

vercel Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
sentry-local Skipped Skipped Nov 7, 2026 2:02am UTC

Request Review

@github-actions github-actions Bot added the risk: medium PR risk score: medium label Nov 7, 2026

@cursor cursor Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/cli/src/lib/sentryclirc.ts Resolved
Comment thread packages/cli/src/lib/sentryclirc.ts Resolved
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>
@sentry-junior sentry-junior Bot changed the title feat(cli): Add a no-persistence mode to the SDK feat(cli): Make SDK calls write no local state by default Nov 7, 2026
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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread packages/cli/src/lib/db/index.ts Resolved
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.
@BYK
BYK enabled auto-merge (squash) November 7, 2026 02:04
@BYK
BYK merged commit b99e345 into main Nov 7, 2026
22 checks passed
@BYK
BYK deleted the feat/cli-no-persistence branch November 7, 2026 02:13
Comment on lines +21 to +54
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),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, and readOnlyView() routes non-SELECT SQL (including UPDATE accounts SET token/refresh_token...) to that statement.
  • persistRefreshedCredential() in auth.ts runs that UPDATE then returns true and resets auth caches with no row-change check.
  • performTokenRefresh() treats that true as success and returns the new tokens only in-memory; cli.db still holds the pre-refresh row.
  • Auth concurrency tests model per-refresh refresh_token rotation; a later refreshToken() re-reads the stale refresh token from DB and fails once the server has rotated it.
  • Default createSentrySDK() sets SENTRY_CLI_NO_PERSISTENCE, so stored OAuth logins hit this path without persist: true.
Also found at 3 additional locations
  • packages/cli/src/lib/sdk-types.ts:69-77
  • packages/cli/src/lib/db/index.ts:21-21
  • packages/cli/test/lib/sdk-no-persistence.test.ts:217-229

Identified by Warden · code-review · PQJ-QG5

BYK added a commit to getsentry/sentry-javascript that referenced this pull request Nov 7, 2026
**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>

This branch was previously deployed

1 inactive deployment
Preview — 7d093360 Deployed Nov 7, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: medium PR risk score: medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a no-persistence mode to the CLI SDK

2 participants