Skip to content

Commit f2058cc

Browse files
betegonclaudegithub-actions[bot]
authored
fix(dsn): make code scanner monorepo-aware and extend --fresh to bypass DSN cache (#420)
## Summary The DSN code scanner missed files inside monorepo packages because `MAX_SCAN_DEPTH=2` couldn't reach past `packages/<name>/src/`. For example, in the spotlight monorepo only 1 of 3 DSNs was detected — `packages/website/sentry.client.config.mjs` at depth 2 was found, but `packages/spotlight/src/instrument.ts` at depth 3 was not. Even after fixing the scanner, stale cached results survive because mtime validation only checks files that were *previously found* — files missed by the old depth limit have no cache entries. Users had no way to force a re-scan. ## Changes **Scanner fix:** - Bump `MAX_SCAN_DEPTH` from 2 to 3 (also helps non-monorepo projects with deeper configs like `src/lib/config/sentry.ts`) - Reset depth to 0 when entering a monorepo package directory (`packages/*`, `apps/*`, `libs/*`, `services/*`, `modules/*`), giving each package its own depth-3 scanning budget - Uses the existing `MONOREPO_ROOTS` constant from `types.ts`, matching how the env file scanner already handles monorepos **Cache bypass (`--fresh`):** - Add `disableDsnCache()`/`enableDsnCache()` following the same pattern as `disableResponseCache()` - Wire into `applyFreshFlag()` so `--fresh` now bypasses both HTTP response cache and DSN detection cache - Cache writes still proceed when disabled, so the re-scanned result gets stored for next time ## Test plan - [x] Added test: finds DSNs in monorepo packages deeper than MAX_SCAN_DEPTH - [x] Added test: finds DSNs from multiple monorepo packages with correct `packagePath` - [x] Added tests: `getCachedDsn`/`getCachedDetection` return undefined when cache disabled, writes persist - [x] All code-scanner and dsn-cache tests pass - [x] Typecheck passes - [x] Lint passes - [ ] Manual test: run `sentry issue list --fresh` from spotlight monorepo, verify multiple projects detected --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent a6d62d7 commit f2058cc

8 files changed

Lines changed: 246 additions & 34 deletions

File tree

plugins/sentry-cli/skills/sentry-cli/SKILL.md

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ View authentication status
9595

9696
**Flags:**
9797
- `--show-token - Show the stored token (masked by default)`
98-
- `-f, --fresh - Bypass cache and fetch fresh data`
98+
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
9999
- `--json - Output as JSON`
100100
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
101101

@@ -114,7 +114,7 @@ Print the stored authentication token
114114
Show the currently authenticated user
115115

116116
**Flags:**
117-
- `-f, --fresh - Bypass cache and fetch fresh data`
117+
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
118118
- `--json - Output as JSON`
119119
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
120120

@@ -128,7 +128,7 @@ List organizations
128128

129129
**Flags:**
130130
- `-n, --limit <value> - Maximum number of organizations to list - (default: "30")`
131-
- `-f, --fresh - Bypass cache and fetch fresh data`
131+
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
132132
- `--json - Output as JSON`
133133
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
134134

@@ -146,7 +146,7 @@ View details of an organization
146146

147147
**Flags:**
148148
- `-w, --web - Open in browser`
149-
- `-f, --fresh - Bypass cache and fetch fresh data`
149+
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
150150
- `--json - Output as JSON`
151151
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
152152

@@ -182,7 +182,7 @@ List projects
182182
- `-n, --limit <value> - Maximum number of projects to list - (default: "30")`
183183
- `-c, --cursor <value> - Pagination cursor (use "last" to continue from previous page)`
184184
- `-p, --platform <value> - Filter by platform (e.g., javascript, python)`
185-
- `-f, --fresh - Bypass cache and fetch fresh data`
185+
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
186186
- `--json - Output as JSON`
187187
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
188188

@@ -205,7 +205,7 @@ View details of a project
205205

206206
**Flags:**
207207
- `-w, --web - Open in browser`
208-
- `-f, --fresh - Bypass cache and fetch fresh data`
208+
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
209209
- `--json - Output as JSON`
210210
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
211211

@@ -240,7 +240,7 @@ List issues in a project
240240
- `-s, --sort <value> - Sort by: date, new, freq, user - (default: "date")`
241241
- `-t, --period <value> - Time period for issue activity (e.g. 24h, 14d, 90d) - (default: "90d")`
242242
- `-c, --cursor <value> - Pagination cursor for <org>/ or multi-target modes (use "last" to continue)`
243-
- `-f, --fresh - Bypass cache and fetch fresh data`
243+
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
244244
- `--compact - Single-line rows for compact output (auto-detects if omitted)`
245245
- `--json - Output as JSON`
246246
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
@@ -287,7 +287,7 @@ Analyze an issue's root cause using Seer AI
287287

288288
**Flags:**
289289
- `--force - Force new analysis even if one exists`
290-
- `-f, --fresh - Bypass cache and fetch fresh data`
290+
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
291291
- `--json - Output as JSON`
292292
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
293293

@@ -316,7 +316,7 @@ Generate a solution plan using Seer AI
316316
**Flags:**
317317
- `--cause <value> - Root cause ID to plan (required if multiple causes exist)`
318318
- `--force - Force new plan even if one exists`
319-
- `-f, --fresh - Bypass cache and fetch fresh data`
319+
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
320320
- `--json - Output as JSON`
321321
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
322322

@@ -345,7 +345,7 @@ View details of a specific issue
345345
**Flags:**
346346
- `-w, --web - Open in browser`
347347
- `--spans <value> - Span tree depth limit (number, "all" for unlimited, "no" to disable) - (default: "3")`
348-
- `-f, --fresh - Bypass cache and fetch fresh data`
348+
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
349349
- `--json - Output as JSON`
350350
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
351351

@@ -374,7 +374,7 @@ View details of a specific event
374374
**Flags:**
375375
- `-w, --web - Open in browser`
376376
- `--spans <value> - Span tree depth limit (number, "all" for unlimited, "no" to disable) - (default: "3")`
377-
- `-f, --fresh - Bypass cache and fetch fresh data`
377+
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
378378
- `--json - Output as JSON`
379379
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
380380

@@ -508,7 +508,7 @@ List repositories
508508
**Flags:**
509509
- `-n, --limit <value> - Maximum number of repositories to list - (default: "30")`
510510
- `-c, --cursor <value> - Pagination cursor (use "last" to continue from previous page)`
511-
- `-f, --fresh - Bypass cache and fetch fresh data`
511+
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
512512
- `--json - Output as JSON`
513513
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
514514

@@ -523,7 +523,7 @@ List teams
523523
**Flags:**
524524
- `-n, --limit <value> - Maximum number of teams to list - (default: "30")`
525525
- `-c, --cursor <value> - Pagination cursor (use "last" to continue from previous page)`
526-
- `-f, --fresh - Bypass cache and fetch fresh data`
526+
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
527527
- `--json - Output as JSON`
528528
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
529529

@@ -555,7 +555,7 @@ List logs from a project
555555
- `-q, --query <value> - Filter query (Sentry search syntax)`
556556
- `-f, --follow <value> - Stream logs (optionally specify poll interval in seconds)`
557557
- `--trace <value> - Filter logs by trace ID (32-character hex string)`
558-
- `--fresh - Bypass cache and fetch fresh data`
558+
- `--fresh - Bypass cache, re-detect projects, and fetch fresh data`
559559
- `--json - Output as JSON`
560560
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
561561

@@ -602,7 +602,7 @@ View details of one or more log entries
602602

603603
**Flags:**
604604
- `-w, --web - Open in browser`
605-
- `-f, --fresh - Bypass cache and fetch fresh data`
605+
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
606606
- `--json - Output as JSON`
607607
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
608608

@@ -640,7 +640,7 @@ List recent traces in a project
640640
- `-q, --query <value> - Search query (Sentry search syntax)`
641641
- `-s, --sort <value> - Sort by: date, duration - (default: "date")`
642642
- `-c, --cursor <value> - Pagination cursor (use "last" to continue from previous page)`
643-
- `-f, --fresh - Bypass cache and fetch fresh data`
643+
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
644644
- `--json - Output as JSON`
645645
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
646646

@@ -651,7 +651,7 @@ View details of a specific trace
651651
**Flags:**
652652
- `-w, --web - Open in browser`
653653
- `--spans <value> - Span tree depth limit (number, "all" for unlimited, "no" to disable) - (default: "3")`
654-
- `-f, --fresh - Bypass cache and fetch fresh data`
654+
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
655655
- `--json - Output as JSON`
656656
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
657657

@@ -664,7 +664,7 @@ View logs associated with a trace
664664
- `-t, --period <value> - Time period to search (e.g., "14d", "7d", "24h"). Default: 14d - (default: "14d")`
665665
- `-n, --limit <value> - Number of log entries (1-1000) - (default: "100")`
666666
- `-q, --query <value> - Additional filter query (Sentry search syntax)`
667-
- `-f, --fresh - Bypass cache and fetch fresh data`
667+
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
668668
- `--json - Output as JSON`
669669
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
670670

@@ -716,7 +716,7 @@ List issues in a project
716716
- `-s, --sort <value> - Sort by: date, new, freq, user - (default: "date")`
717717
- `-t, --period <value> - Time period for issue activity (e.g. 24h, 14d, 90d) - (default: "90d")`
718718
- `-c, --cursor <value> - Pagination cursor for <org>/ or multi-target modes (use "last" to continue)`
719-
- `-f, --fresh - Bypass cache and fetch fresh data`
719+
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
720720
- `--compact - Single-line rows for compact output (auto-detects if omitted)`
721721
- `--json - Output as JSON`
722722
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
@@ -731,7 +731,7 @@ List organizations
731731

732732
**Flags:**
733733
- `-n, --limit <value> - Maximum number of organizations to list - (default: "30")`
734-
- `-f, --fresh - Bypass cache and fetch fresh data`
734+
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
735735
- `--json - Output as JSON`
736736
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
737737

@@ -747,7 +747,7 @@ List projects
747747
- `-n, --limit <value> - Maximum number of projects to list - (default: "30")`
748748
- `-c, --cursor <value> - Pagination cursor (use "last" to continue from previous page)`
749749
- `-p, --platform <value> - Filter by platform (e.g., javascript, python)`
750-
- `-f, --fresh - Bypass cache and fetch fresh data`
750+
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
751751
- `--json - Output as JSON`
752752
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
753753

@@ -762,7 +762,7 @@ List repositories
762762
**Flags:**
763763
- `-n, --limit <value> - Maximum number of repositories to list - (default: "30")`
764764
- `-c, --cursor <value> - Pagination cursor (use "last" to continue from previous page)`
765-
- `-f, --fresh - Bypass cache and fetch fresh data`
765+
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
766766
- `--json - Output as JSON`
767767
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
768768

@@ -777,7 +777,7 @@ List teams
777777
**Flags:**
778778
- `-n, --limit <value> - Maximum number of teams to list - (default: "30")`
779779
- `-c, --cursor <value> - Pagination cursor (use "last" to continue from previous page)`
780-
- `-f, --fresh - Bypass cache and fetch fresh data`
780+
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
781781
- `--json - Output as JSON`
782782
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
783783

@@ -794,7 +794,7 @@ List logs from a project
794794
- `-q, --query <value> - Filter query (Sentry search syntax)`
795795
- `-f, --follow <value> - Stream logs (optionally specify poll interval in seconds)`
796796
- `--trace <value> - Filter logs by trace ID (32-character hex string)`
797-
- `--fresh - Bypass cache and fetch fresh data`
797+
- `--fresh - Bypass cache, re-detect projects, and fetch fresh data`
798798
- `--json - Output as JSON`
799799
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
800800

@@ -811,7 +811,7 @@ List recent traces in a project
811811
- `-q, --query <value> - Search query (Sentry search syntax)`
812812
- `-s, --sort <value> - Sort by: date, duration - (default: "date")`
813813
- `-c, --cursor <value> - Pagination cursor (use "last" to continue from previous page)`
814-
- `-f, --fresh - Bypass cache and fetch fresh data`
814+
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
815815
- `--json - Output as JSON`
816816
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
817817

@@ -836,7 +836,7 @@ Show the currently authenticated user
836836
Show the currently authenticated user
837837

838838
**Flags:**
839-
- `-f, --fresh - Bypass cache and fetch fresh data`
839+
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
840840
- `--json - Output as JSON`
841841
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
842842

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/code-scanner.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { logger } from "../logger.js";
2525
import { withTracingSpan } from "../telemetry.js";
2626
import { createDetectedDsn, inferPackagePath, parseDsn } from "./parser.js";
2727
import type { DetectedDsn } from "./types.js";
28+
import { MONOREPO_ROOTS } from "./types.js";
2829

2930
/** Scoped logger for DSN code scanning */
3031
const log = logger.withTag("dsn-scan");
@@ -61,9 +62,12 @@ const CONCURRENCY_LIMIT = 50;
6162
/**
6263
* Maximum depth to scan from project root.
6364
* Depth 0 = files in root directory
64-
* Depth 2 = files in second-level subdirectories (e.g., src/lib/file.ts)
65+
* Depth 3 = files in third-level subdirectories (e.g., src/lib/config/sentry.ts)
66+
*
67+
* In monorepos, depth resets to 0 when entering a package directory
68+
* (e.g., packages/spotlight/), giving each package its own depth budget.
6569
*/
66-
const MAX_SCAN_DEPTH = 2;
70+
const MAX_SCAN_DEPTH = 3;
6771

6872
/**
6973
* Directories that are always skipped regardless of .gitignore.
@@ -184,6 +188,19 @@ const normalizePath: (p: string) => string =
184188
? (x) => x
185189
: (x) => x.replaceAll(path.sep, path.posix.sep);
186190

191+
/**
192+
* Check if a relative path is a monorepo package directory.
193+
* Returns true for paths like "packages/frontend", "apps/server", etc.
194+
* (exactly 2 segments where the first matches a MONOREPO_ROOTS entry)
195+
*/
196+
function isMonorepoPackageDir(relativePath: string): boolean {
197+
const segments = relativePath.split("/");
198+
return (
199+
segments.length === 2 &&
200+
MONOREPO_ROOTS.includes(segments[0] as (typeof MONOREPO_ROOTS)[number])
201+
);
202+
}
203+
187204
/**
188205
* Pattern to match Sentry DSN URLs.
189206
* Captures the full DSN including protocol, public key, optional secret key, host, and project ID.
@@ -441,6 +458,7 @@ async function collectFiles(cwd: string, ig: Ignore): Promise<CollectResult> {
441458
const files: string[] = [];
442459
const dirMtimes: Record<string, number> = {};
443460

461+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: recursive directory walk is inherently complex but straightforward
444462
async function walk(dir: string, depth: number): Promise<void> {
445463
if (depth > MAX_SCAN_DEPTH) {
446464
return;
@@ -462,7 +480,8 @@ async function collectFiles(cwd: string, ig: Ignore): Promise<CollectResult> {
462480
}
463481

464482
if (entry.isDirectory()) {
465-
await walk(fullPath, depth + 1);
483+
const nextDepth = isMonorepoPackageDir(relativePath) ? 0 : depth + 1;
484+
await walk(fullPath, nextDepth);
466485
} else if (entry.isFile() && shouldScanFile(entry.name)) {
467486
files.push(relativePath);
468487
}

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/dsn/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,4 +136,9 @@ export const MONOREPO_ROOTS = [
136136
"libs",
137137
"services",
138138
"modules",
139+
"projects",
140+
"plugins",
141+
"sites",
142+
"workers",
143+
"functions",
139144
] as const;

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

0 commit comments

Comments
 (0)