Skip to content

Commit c9eacd0

Browse files
betegonclaude
andcommitted
feat(dsn): extend --fresh flag to bypass DSN detection cache
After the scanner fix, stale cached results (with fewer DSNs) survive because mtime validation only checks files that were previously found. Users had no way to force a re-scan short of manually clearing SQLite. Add disableDsnCache()/enableDsnCache() following the same pattern as disableResponseCache(), and wire it into applyFreshFlag() so --fresh now bypasses both HTTP response cache and DSN detection cache. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 94c276c commit c9eacd0

File tree

4 files changed

+115
-5
lines changed

4 files changed

+115
-5
lines changed

src/lib/db/dsn-cache.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,30 @@ import { runUpsert } from "./utils.js";
1919
/** Cache TTL in milliseconds (24 hours) */
2020
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
2121

22+
/**
23+
* Module-level flag to disable DSN cache reads.
24+
* When true, getCachedDsn() and getCachedDetection() return undefined.
25+
* Cache writes still proceed so the re-scanned result gets stored.
26+
*/
27+
let dsnCacheDisabled = false;
28+
29+
/**
30+
* Disable DSN cache reads for this invocation.
31+
* Called when `--fresh` is set to force a full re-scan.
32+
*/
33+
export function disableDsnCache(): void {
34+
dsnCacheDisabled = true;
35+
}
36+
37+
/**
38+
* Re-enable DSN cache reads after `disableDsnCache()` was called.
39+
* Only needed in tests to prevent one test's `--fresh` flag from
40+
* leaking into subsequent tests.
41+
*/
42+
export function enableDsnCache(): void {
43+
dsnCacheDisabled = false;
44+
}
45+
2246
/** Row type matching the dsn_cache table schema (including v4 columns) */
2347
type DsnCacheRow = {
2448
directory: string;
@@ -116,6 +140,10 @@ function touchCacheEntry(directory: string): void {
116140
export async function getCachedDsn(
117141
directory: string
118142
): Promise<CachedDsnEntry | undefined> {
143+
if (dsnCacheDisabled) {
144+
return;
145+
}
146+
119147
const db = getDatabase();
120148

121149
const row = db
@@ -301,6 +329,10 @@ async function validateDirMtimes(
301329
export async function getCachedDetection(
302330
projectRoot: string
303331
): Promise<CachedDetection | undefined> {
332+
if (dsnCacheDisabled) {
333+
return;
334+
}
335+
304336
const db = getDatabase();
305337

306338
const row = db

src/lib/dsn/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
// Cache Management
2121
export {
2222
clearDsnCache,
23+
disableDsnCache,
24+
enableDsnCache,
2325
getCachedDsn,
2426
setCachedDsn,
2527
updateCachedResolution,

src/lib/list-command.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type { Aliases, Command, CommandContext } from "@stricli/core";
1717
import type { SentryContext } from "../context.js";
1818
import { parseOrgProjectArg } from "./arg-parsing.js";
1919
import { buildCommand, numberParser } from "./command.js";
20+
import { disableDsnCache } from "./dsn/index.js";
2021
import { warning } from "./formatters/colors.js";
2122
import type { CommandOutput, OutputConfig } from "./formatters/output.js";
2223
import {
@@ -110,7 +111,7 @@ export const LIST_JSON_FLAG = {
110111
*/
111112
export const FRESH_FLAG = {
112113
kind: "boolean" as const,
113-
brief: "Bypass cache and fetch fresh data",
114+
brief: "Bypass cache, re-detect projects, and fetch fresh data",
114115
default: false,
115116
} as const;
116117

@@ -140,6 +141,7 @@ export const FRESH_ALIASES = { f: "fresh" } as const;
140141
export function applyFreshFlag(flags: { readonly fresh: boolean }): void {
141142
if (flags.fresh) {
142143
disableResponseCache();
144+
disableDsnCache();
143145
}
144146
}
145147

test/lib/db/dsn-cache.test.ts

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { mkdirSync, writeFileSync } from "node:fs";
99
import { join } from "node:path";
1010
import {
1111
clearDsnCache,
12+
disableDsnCache,
13+
enableDsnCache,
1214
getCachedDetection,
1315
getCachedDsn,
1416
setCachedDetection,
@@ -36,6 +38,7 @@ beforeEach(async () => {
3638
});
3739

3840
afterEach(async () => {
41+
enableDsnCache();
3942
delete process.env.SENTRY_CLI_CONFIG_DIR;
4043
await cleanupTestDir(testConfigDir);
4144
});
@@ -173,10 +176,6 @@ describe("clearDsnCache", () => {
173176
});
174177
});
175178

176-
// =============================================================================
177-
// Full Detection Cache Tests (v4 functionality)
178-
// =============================================================================
179-
180179
const createTestDsn = (overrides: Partial<DetectedDsn> = {}): DetectedDsn => ({
181180
protocol: "https",
182181
publicKey: "testkey",
@@ -189,6 +188,81 @@ const createTestDsn = (overrides: Partial<DetectedDsn> = {}): DetectedDsn => ({
189188
...overrides,
190189
});
191190

191+
// =============================================================================
192+
// Cache Bypass Tests (--fresh flag support)
193+
// =============================================================================
194+
195+
describe("disableDsnCache / enableDsnCache", () => {
196+
test("getCachedDsn returns undefined when cache is disabled", async () => {
197+
await setCachedDsn(testProjectDir, {
198+
dsn: "https://abc@o123.ingest.sentry.io/456",
199+
projectId: "456",
200+
orgId: "123",
201+
source: "code",
202+
});
203+
204+
// Verify it exists before disabling
205+
expect(await getCachedDsn(testProjectDir)).toBeDefined();
206+
207+
disableDsnCache();
208+
expect(await getCachedDsn(testProjectDir)).toBeUndefined();
209+
210+
// Re-enable and verify it's still there
211+
enableDsnCache();
212+
expect(await getCachedDsn(testProjectDir)).toBeDefined();
213+
});
214+
215+
test("getCachedDetection returns undefined when cache is disabled", async () => {
216+
const testDsn = createTestDsn();
217+
const sourceMtimes = {
218+
"src/app.ts": Bun.file(join(testProjectDir, "src/app.ts")).lastModified,
219+
};
220+
const { stat } = await import("node:fs/promises");
221+
const rootStats = await stat(testProjectDir);
222+
const rootDirMtime = Math.floor(rootStats.mtimeMs);
223+
224+
await setCachedDetection(testProjectDir, {
225+
fingerprint: "test-fp",
226+
allDsns: [testDsn],
227+
sourceMtimes,
228+
dirMtimes: {},
229+
rootDirMtime,
230+
});
231+
232+
// Verify it exists before disabling
233+
expect(await getCachedDetection(testProjectDir)).toBeDefined();
234+
235+
disableDsnCache();
236+
expect(await getCachedDetection(testProjectDir)).toBeUndefined();
237+
238+
enableDsnCache();
239+
expect(await getCachedDetection(testProjectDir)).toBeDefined();
240+
});
241+
242+
test("cache writes still work when disabled", async () => {
243+
disableDsnCache();
244+
245+
// Write while disabled
246+
await setCachedDsn(testProjectDir, {
247+
dsn: "https://abc@o123.ingest.sentry.io/456",
248+
projectId: "456",
249+
source: "code",
250+
});
251+
252+
// Can't read while disabled
253+
expect(await getCachedDsn(testProjectDir)).toBeUndefined();
254+
255+
// Re-enable and verify the write persisted
256+
enableDsnCache();
257+
const result = await getCachedDsn(testProjectDir);
258+
expect(result?.dsn).toBe("https://abc@o123.ingest.sentry.io/456");
259+
});
260+
});
261+
262+
// =============================================================================
263+
// Full Detection Cache Tests (v4 functionality)
264+
// =============================================================================
265+
192266
describe("getCachedDetection", () => {
193267
test("returns undefined when no cache entry exists", async () => {
194268
const result = await getCachedDetection("/nonexistent/path");

0 commit comments

Comments
 (0)