From 94c276cfc60f2655e0ab1ec62ee2d47e61bb721b Mon Sep 17 00:00:00 2001 From: betegon Date: Fri, 13 Mar 2026 17:02:34 +0100 Subject: [PATCH 1/5] fix(dsn): make code scanner monorepo-aware with depth reset The code scanner used MAX_SCAN_DEPTH=2 which couldn't reach files inside monorepo packages (e.g., packages/spotlight/src/instrument.ts at depth 3). The packages/ prefix consumed the full depth budget, leaving nothing for src/. Bump MAX_SCAN_DEPTH to 3 and reset depth to 0 when entering a monorepo package directory (packages/*, apps/*, etc.), giving each package its own depth budget. This matches how the env file scanner already handles monorepos. Co-Authored-By: Claude Opus 4.6 --- src/lib/dsn/code-scanner.ts | 25 +++++++++++++++--- test/lib/dsn/code-scanner.test.ts | 44 +++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/src/lib/dsn/code-scanner.ts b/src/lib/dsn/code-scanner.ts index 9623cddf..926f5818 100644 --- a/src/lib/dsn/code-scanner.ts +++ b/src/lib/dsn/code-scanner.ts @@ -25,6 +25,7 @@ import { logger } from "../logger.js"; import { withTracingSpan } from "../telemetry.js"; import { createDetectedDsn, inferPackagePath, parseDsn } from "./parser.js"; import type { DetectedDsn } from "./types.js"; +import { MONOREPO_ROOTS } from "./types.js"; /** Scoped logger for DSN code scanning */ const log = logger.withTag("dsn-scan"); @@ -61,9 +62,12 @@ const CONCURRENCY_LIMIT = 50; /** * Maximum depth to scan from project root. * Depth 0 = files in root directory - * Depth 2 = files in second-level subdirectories (e.g., src/lib/file.ts) + * Depth 3 = files in third-level subdirectories (e.g., src/lib/config/sentry.ts) + * + * In monorepos, depth resets to 0 when entering a package directory + * (e.g., packages/spotlight/), giving each package its own depth budget. */ -const MAX_SCAN_DEPTH = 2; +const MAX_SCAN_DEPTH = 3; /** * Directories that are always skipped regardless of .gitignore. @@ -184,6 +188,19 @@ const normalizePath: (p: string) => string = ? (x) => x : (x) => x.replaceAll(path.sep, path.posix.sep); +/** + * Check if a relative path is a monorepo package directory. + * Returns true for paths like "packages/frontend", "apps/server", etc. + * (exactly 2 segments where the first matches a MONOREPO_ROOTS entry) + */ +function isMonorepoPackageDir(relativePath: string): boolean { + const segments = relativePath.split("/"); + return ( + segments.length === 2 && + MONOREPO_ROOTS.includes(segments[0] as (typeof MONOREPO_ROOTS)[number]) + ); +} + /** * Pattern to match Sentry DSN URLs. * 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 { const files: string[] = []; const dirMtimes: Record = {}; + // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: recursive directory walk is inherently complex but straightforward async function walk(dir: string, depth: number): Promise { if (depth > MAX_SCAN_DEPTH) { return; @@ -462,7 +480,8 @@ async function collectFiles(cwd: string, ig: Ignore): Promise { } if (entry.isDirectory()) { - await walk(fullPath, depth + 1); + const nextDepth = isMonorepoPackageDir(relativePath) ? 0 : depth + 1; + await walk(fullPath, nextDepth); } else if (entry.isFile() && shouldScanFile(entry.name)) { files.push(relativePath); } diff --git a/test/lib/dsn/code-scanner.test.ts b/test/lib/dsn/code-scanner.test.ts index 5d07aa57..4bfdc5c5 100644 --- a/test/lib/dsn/code-scanner.test.ts +++ b/test/lib/dsn/code-scanner.test.ts @@ -418,6 +418,50 @@ describe("Code Scanner", () => { expect(result.dsns).toEqual([]); }); + test("finds DSNs in monorepo packages deeper than MAX_SCAN_DEPTH", async () => { + // packages/spotlight/src/instrument.ts is depth 3 from root, + // but with monorepo depth reset, packages/spotlight/ resets to 0 + // so src/instrument.ts is only depth 1 from the package root + mkdirSync(join(testDir, "packages/spotlight/src"), { recursive: true }); + writeFileSync( + join(testDir, "packages/spotlight/src/instrument.ts"), + 'Sentry.init({ dsn: "https://spotlight@o123.ingest.sentry.io/111" });' + ); + + const result = await scanCodeForDsns(testDir); + expect(result.dsns).toHaveLength(1); + expect(result.dsns[0]?.raw).toBe( + "https://spotlight@o123.ingest.sentry.io/111" + ); + expect(result.dsns[0]?.packagePath).toBe("packages/spotlight"); + }); + + test("finds DSNs from multiple monorepo packages", async () => { + mkdirSync(join(testDir, "packages/frontend/src"), { recursive: true }); + mkdirSync(join(testDir, "packages/backend/src"), { recursive: true }); + writeFileSync( + join(testDir, "packages/frontend/src/sentry.ts"), + 'const DSN = "https://fe@o123.ingest.sentry.io/111";' + ); + writeFileSync( + join(testDir, "packages/backend/src/sentry.ts"), + 'const DSN = "https://be@o456.ingest.sentry.io/222";' + ); + + const result = await scanCodeForDsns(testDir); + expect(result.dsns).toHaveLength(2); + + const dsns = result.dsns.map((d) => d.raw); + expect(dsns).toContain("https://fe@o123.ingest.sentry.io/111"); + expect(dsns).toContain("https://be@o456.ingest.sentry.io/222"); + + // Verify packagePath is set correctly for each + const feResult = result.dsns.find((d) => d.raw.includes("fe@")); + const beResult = result.dsns.find((d) => d.raw.includes("be@")); + expect(feResult?.packagePath).toBe("packages/frontend"); + expect(beResult?.packagePath).toBe("packages/backend"); + }); + test("gracefully handles unreadable files", async () => { const { chmodSync } = require("node:fs") as typeof import("node:fs"); const filePath = join(testDir, "secret.ts"); From c9eacd07c588153bbae1ab4e242fc0430b8986a9 Mon Sep 17 00:00:00 2001 From: betegon Date: Fri, 13 Mar 2026 17:29:12 +0100 Subject: [PATCH 2/5] 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 --- src/lib/db/dsn-cache.ts | 32 ++++++++++++++ src/lib/dsn/index.ts | 2 + src/lib/list-command.ts | 4 +- test/lib/db/dsn-cache.test.ts | 82 +++++++++++++++++++++++++++++++++-- 4 files changed, 115 insertions(+), 5 deletions(-) diff --git a/src/lib/db/dsn-cache.ts b/src/lib/db/dsn-cache.ts index fe12e0f8..17f3d523 100644 --- a/src/lib/db/dsn-cache.ts +++ b/src/lib/db/dsn-cache.ts @@ -19,6 +19,30 @@ import { runUpsert } from "./utils.js"; /** Cache TTL in milliseconds (24 hours) */ const CACHE_TTL_MS = 24 * 60 * 60 * 1000; +/** + * Module-level flag to disable DSN cache reads. + * When true, getCachedDsn() and getCachedDetection() return undefined. + * Cache writes still proceed so the re-scanned result gets stored. + */ +let dsnCacheDisabled = false; + +/** + * Disable DSN cache reads for this invocation. + * Called when `--fresh` is set to force a full re-scan. + */ +export function disableDsnCache(): void { + dsnCacheDisabled = true; +} + +/** + * Re-enable DSN cache reads after `disableDsnCache()` was called. + * Only needed in tests to prevent one test's `--fresh` flag from + * leaking into subsequent tests. + */ +export function enableDsnCache(): void { + dsnCacheDisabled = false; +} + /** Row type matching the dsn_cache table schema (including v4 columns) */ type DsnCacheRow = { directory: string; @@ -116,6 +140,10 @@ function touchCacheEntry(directory: string): void { export async function getCachedDsn( directory: string ): Promise { + if (dsnCacheDisabled) { + return; + } + const db = getDatabase(); const row = db @@ -301,6 +329,10 @@ async function validateDirMtimes( export async function getCachedDetection( projectRoot: string ): Promise { + if (dsnCacheDisabled) { + return; + } + const db = getDatabase(); const row = db diff --git a/src/lib/dsn/index.ts b/src/lib/dsn/index.ts index d0a687c9..fe431ba6 100644 --- a/src/lib/dsn/index.ts +++ b/src/lib/dsn/index.ts @@ -20,6 +20,8 @@ // Cache Management export { clearDsnCache, + disableDsnCache, + enableDsnCache, getCachedDsn, setCachedDsn, updateCachedResolution, diff --git a/src/lib/list-command.ts b/src/lib/list-command.ts index 1854672e..e4468053 100644 --- a/src/lib/list-command.ts +++ b/src/lib/list-command.ts @@ -17,6 +17,7 @@ import type { Aliases, Command, CommandContext } from "@stricli/core"; import type { SentryContext } from "../context.js"; import { parseOrgProjectArg } from "./arg-parsing.js"; import { buildCommand, numberParser } from "./command.js"; +import { disableDsnCache } from "./dsn/index.js"; import { warning } from "./formatters/colors.js"; import type { CommandOutput, OutputConfig } from "./formatters/output.js"; import { @@ -110,7 +111,7 @@ export const LIST_JSON_FLAG = { */ export const FRESH_FLAG = { kind: "boolean" as const, - brief: "Bypass cache and fetch fresh data", + brief: "Bypass cache, re-detect projects, and fetch fresh data", default: false, } as const; @@ -140,6 +141,7 @@ export const FRESH_ALIASES = { f: "fresh" } as const; export function applyFreshFlag(flags: { readonly fresh: boolean }): void { if (flags.fresh) { disableResponseCache(); + disableDsnCache(); } } diff --git a/test/lib/db/dsn-cache.test.ts b/test/lib/db/dsn-cache.test.ts index f0838778..6815f70e 100644 --- a/test/lib/db/dsn-cache.test.ts +++ b/test/lib/db/dsn-cache.test.ts @@ -9,6 +9,8 @@ import { mkdirSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { clearDsnCache, + disableDsnCache, + enableDsnCache, getCachedDetection, getCachedDsn, setCachedDetection, @@ -36,6 +38,7 @@ beforeEach(async () => { }); afterEach(async () => { + enableDsnCache(); delete process.env.SENTRY_CLI_CONFIG_DIR; await cleanupTestDir(testConfigDir); }); @@ -173,10 +176,6 @@ describe("clearDsnCache", () => { }); }); -// ============================================================================= -// Full Detection Cache Tests (v4 functionality) -// ============================================================================= - const createTestDsn = (overrides: Partial = {}): DetectedDsn => ({ protocol: "https", publicKey: "testkey", @@ -189,6 +188,81 @@ const createTestDsn = (overrides: Partial = {}): DetectedDsn => ({ ...overrides, }); +// ============================================================================= +// Cache Bypass Tests (--fresh flag support) +// ============================================================================= + +describe("disableDsnCache / enableDsnCache", () => { + test("getCachedDsn returns undefined when cache is disabled", async () => { + await setCachedDsn(testProjectDir, { + dsn: "https://abc@o123.ingest.sentry.io/456", + projectId: "456", + orgId: "123", + source: "code", + }); + + // Verify it exists before disabling + expect(await getCachedDsn(testProjectDir)).toBeDefined(); + + disableDsnCache(); + expect(await getCachedDsn(testProjectDir)).toBeUndefined(); + + // Re-enable and verify it's still there + enableDsnCache(); + expect(await getCachedDsn(testProjectDir)).toBeDefined(); + }); + + test("getCachedDetection returns undefined when cache is disabled", async () => { + const testDsn = createTestDsn(); + const sourceMtimes = { + "src/app.ts": Bun.file(join(testProjectDir, "src/app.ts")).lastModified, + }; + const { stat } = await import("node:fs/promises"); + const rootStats = await stat(testProjectDir); + const rootDirMtime = Math.floor(rootStats.mtimeMs); + + await setCachedDetection(testProjectDir, { + fingerprint: "test-fp", + allDsns: [testDsn], + sourceMtimes, + dirMtimes: {}, + rootDirMtime, + }); + + // Verify it exists before disabling + expect(await getCachedDetection(testProjectDir)).toBeDefined(); + + disableDsnCache(); + expect(await getCachedDetection(testProjectDir)).toBeUndefined(); + + enableDsnCache(); + expect(await getCachedDetection(testProjectDir)).toBeDefined(); + }); + + test("cache writes still work when disabled", async () => { + disableDsnCache(); + + // Write while disabled + await setCachedDsn(testProjectDir, { + dsn: "https://abc@o123.ingest.sentry.io/456", + projectId: "456", + source: "code", + }); + + // Can't read while disabled + expect(await getCachedDsn(testProjectDir)).toBeUndefined(); + + // Re-enable and verify the write persisted + enableDsnCache(); + const result = await getCachedDsn(testProjectDir); + expect(result?.dsn).toBe("https://abc@o123.ingest.sentry.io/456"); + }); +}); + +// ============================================================================= +// Full Detection Cache Tests (v4 functionality) +// ============================================================================= + describe("getCachedDetection", () => { test("returns undefined when no cache entry exists", async () => { const result = await getCachedDetection("/nonexistent/path"); From f8c366c0bd905dbda59cc9a7205283b08f5a4306 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 13 Mar 2026 16:29:42 +0000 Subject: [PATCH 3/5] chore: regenerate SKILL.md --- plugins/sentry-cli/skills/sentry-cli/SKILL.md | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/plugins/sentry-cli/skills/sentry-cli/SKILL.md b/plugins/sentry-cli/skills/sentry-cli/SKILL.md index 4814dc9a..387af9dc 100644 --- a/plugins/sentry-cli/skills/sentry-cli/SKILL.md +++ b/plugins/sentry-cli/skills/sentry-cli/SKILL.md @@ -95,7 +95,7 @@ View authentication status **Flags:** - `--show-token - Show the stored token (masked by default)` -- `-f, --fresh - Bypass cache and fetch fresh data` +- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `--json - Output as JSON` - `--fields - Comma-separated fields to include in JSON output (dot.notation supported)` @@ -114,7 +114,7 @@ Print the stored authentication token Show the currently authenticated user **Flags:** -- `-f, --fresh - Bypass cache and fetch fresh data` +- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `--json - Output as JSON` - `--fields - Comma-separated fields to include in JSON output (dot.notation supported)` @@ -128,7 +128,7 @@ List organizations **Flags:** - `-n, --limit - Maximum number of organizations to list - (default: "30")` -- `-f, --fresh - Bypass cache and fetch fresh data` +- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `--json - Output as JSON` - `--fields - Comma-separated fields to include in JSON output (dot.notation supported)` @@ -146,7 +146,7 @@ View details of an organization **Flags:** - `-w, --web - Open in browser` -- `-f, --fresh - Bypass cache and fetch fresh data` +- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `--json - Output as JSON` - `--fields - Comma-separated fields to include in JSON output (dot.notation supported)` @@ -182,7 +182,7 @@ List projects - `-n, --limit - Maximum number of projects to list - (default: "30")` - `-c, --cursor - Pagination cursor (use "last" to continue from previous page)` - `-p, --platform - Filter by platform (e.g., javascript, python)` -- `-f, --fresh - Bypass cache and fetch fresh data` +- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `--json - Output as JSON` - `--fields - Comma-separated fields to include in JSON output (dot.notation supported)` @@ -205,7 +205,7 @@ View details of a project **Flags:** - `-w, --web - Open in browser` -- `-f, --fresh - Bypass cache and fetch fresh data` +- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `--json - Output as JSON` - `--fields - Comma-separated fields to include in JSON output (dot.notation supported)` @@ -240,7 +240,7 @@ List issues in a project - `-s, --sort - Sort by: date, new, freq, user - (default: "date")` - `-t, --period - Time period for issue activity (e.g. 24h, 14d, 90d) - (default: "90d")` - `-c, --cursor - Pagination cursor for / or multi-target modes (use "last" to continue)` -- `-f, --fresh - Bypass cache and fetch fresh data` +- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `--compact - Single-line rows for compact output (auto-detects if omitted)` - `--json - Output as JSON` - `--fields - Comma-separated fields to include in JSON output (dot.notation supported)` @@ -287,7 +287,7 @@ Analyze an issue's root cause using Seer AI **Flags:** - `--force - Force new analysis even if one exists` -- `-f, --fresh - Bypass cache and fetch fresh data` +- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `--json - Output as JSON` - `--fields - Comma-separated fields to include in JSON output (dot.notation supported)` @@ -316,7 +316,7 @@ Generate a solution plan using Seer AI **Flags:** - `--cause - Root cause ID to plan (required if multiple causes exist)` - `--force - Force new plan even if one exists` -- `-f, --fresh - Bypass cache and fetch fresh data` +- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `--json - Output as JSON` - `--fields - Comma-separated fields to include in JSON output (dot.notation supported)` @@ -345,7 +345,7 @@ View details of a specific issue **Flags:** - `-w, --web - Open in browser` - `--spans - Span tree depth limit (number, "all" for unlimited, "no" to disable) - (default: "3")` -- `-f, --fresh - Bypass cache and fetch fresh data` +- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `--json - Output as JSON` - `--fields - Comma-separated fields to include in JSON output (dot.notation supported)` @@ -374,7 +374,7 @@ View details of a specific event **Flags:** - `-w, --web - Open in browser` - `--spans - Span tree depth limit (number, "all" for unlimited, "no" to disable) - (default: "3")` -- `-f, --fresh - Bypass cache and fetch fresh data` +- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `--json - Output as JSON` - `--fields - Comma-separated fields to include in JSON output (dot.notation supported)` @@ -508,7 +508,7 @@ List repositories **Flags:** - `-n, --limit - Maximum number of repositories to list - (default: "30")` - `-c, --cursor - Pagination cursor (use "last" to continue from previous page)` -- `-f, --fresh - Bypass cache and fetch fresh data` +- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `--json - Output as JSON` - `--fields - Comma-separated fields to include in JSON output (dot.notation supported)` @@ -523,7 +523,7 @@ List teams **Flags:** - `-n, --limit - Maximum number of teams to list - (default: "30")` - `-c, --cursor - Pagination cursor (use "last" to continue from previous page)` -- `-f, --fresh - Bypass cache and fetch fresh data` +- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `--json - Output as JSON` - `--fields - Comma-separated fields to include in JSON output (dot.notation supported)` @@ -555,7 +555,7 @@ List logs from a project - `-q, --query - Filter query (Sentry search syntax)` - `-f, --follow - Stream logs (optionally specify poll interval in seconds)` - `--trace - Filter logs by trace ID (32-character hex string)` -- `--fresh - Bypass cache and fetch fresh data` +- `--fresh - Bypass cache, re-detect projects, and fetch fresh data` - `--json - Output as JSON` - `--fields - Comma-separated fields to include in JSON output (dot.notation supported)` @@ -602,7 +602,7 @@ View details of one or more log entries **Flags:** - `-w, --web - Open in browser` -- `-f, --fresh - Bypass cache and fetch fresh data` +- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `--json - Output as JSON` - `--fields - Comma-separated fields to include in JSON output (dot.notation supported)` @@ -640,7 +640,7 @@ List recent traces in a project - `-q, --query - Search query (Sentry search syntax)` - `-s, --sort - Sort by: date, duration - (default: "date")` - `-c, --cursor - Pagination cursor (use "last" to continue from previous page)` -- `-f, --fresh - Bypass cache and fetch fresh data` +- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `--json - Output as JSON` - `--fields - Comma-separated fields to include in JSON output (dot.notation supported)` @@ -651,7 +651,7 @@ View details of a specific trace **Flags:** - `-w, --web - Open in browser` - `--spans - Span tree depth limit (number, "all" for unlimited, "no" to disable) - (default: "3")` -- `-f, --fresh - Bypass cache and fetch fresh data` +- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `--json - Output as JSON` - `--fields - Comma-separated fields to include in JSON output (dot.notation supported)` @@ -664,7 +664,7 @@ View logs associated with a trace - `-t, --period - Time period to search (e.g., "14d", "7d", "24h"). Default: 14d - (default: "14d")` - `-n, --limit - Number of log entries (1-1000) - (default: "100")` - `-q, --query - Additional filter query (Sentry search syntax)` -- `-f, --fresh - Bypass cache and fetch fresh data` +- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `--json - Output as JSON` - `--fields - Comma-separated fields to include in JSON output (dot.notation supported)` @@ -716,7 +716,7 @@ List issues in a project - `-s, --sort - Sort by: date, new, freq, user - (default: "date")` - `-t, --period - Time period for issue activity (e.g. 24h, 14d, 90d) - (default: "90d")` - `-c, --cursor - Pagination cursor for / or multi-target modes (use "last" to continue)` -- `-f, --fresh - Bypass cache and fetch fresh data` +- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `--compact - Single-line rows for compact output (auto-detects if omitted)` - `--json - Output as JSON` - `--fields - Comma-separated fields to include in JSON output (dot.notation supported)` @@ -731,7 +731,7 @@ List organizations **Flags:** - `-n, --limit - Maximum number of organizations to list - (default: "30")` -- `-f, --fresh - Bypass cache and fetch fresh data` +- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `--json - Output as JSON` - `--fields - Comma-separated fields to include in JSON output (dot.notation supported)` @@ -747,7 +747,7 @@ List projects - `-n, --limit - Maximum number of projects to list - (default: "30")` - `-c, --cursor - Pagination cursor (use "last" to continue from previous page)` - `-p, --platform - Filter by platform (e.g., javascript, python)` -- `-f, --fresh - Bypass cache and fetch fresh data` +- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `--json - Output as JSON` - `--fields - Comma-separated fields to include in JSON output (dot.notation supported)` @@ -762,7 +762,7 @@ List repositories **Flags:** - `-n, --limit - Maximum number of repositories to list - (default: "30")` - `-c, --cursor - Pagination cursor (use "last" to continue from previous page)` -- `-f, --fresh - Bypass cache and fetch fresh data` +- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `--json - Output as JSON` - `--fields - Comma-separated fields to include in JSON output (dot.notation supported)` @@ -777,7 +777,7 @@ List teams **Flags:** - `-n, --limit - Maximum number of teams to list - (default: "30")` - `-c, --cursor - Pagination cursor (use "last" to continue from previous page)` -- `-f, --fresh - Bypass cache and fetch fresh data` +- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `--json - Output as JSON` - `--fields - Comma-separated fields to include in JSON output (dot.notation supported)` @@ -794,7 +794,7 @@ List logs from a project - `-q, --query - Filter query (Sentry search syntax)` - `-f, --follow - Stream logs (optionally specify poll interval in seconds)` - `--trace - Filter logs by trace ID (32-character hex string)` -- `--fresh - Bypass cache and fetch fresh data` +- `--fresh - Bypass cache, re-detect projects, and fetch fresh data` - `--json - Output as JSON` - `--fields - Comma-separated fields to include in JSON output (dot.notation supported)` @@ -811,7 +811,7 @@ List recent traces in a project - `-q, --query - Search query (Sentry search syntax)` - `-s, --sort - Sort by: date, duration - (default: "date")` - `-c, --cursor - Pagination cursor (use "last" to continue from previous page)` -- `-f, --fresh - Bypass cache and fetch fresh data` +- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `--json - Output as JSON` - `--fields - Comma-separated fields to include in JSON output (dot.notation supported)` @@ -836,7 +836,7 @@ Show the currently authenticated user Show the currently authenticated user **Flags:** -- `-f, --fresh - Bypass cache and fetch fresh data` +- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data` - `--json - Output as JSON` - `--fields - Comma-separated fields to include in JSON output (dot.notation supported)` From 34b39782fa77b17bd4e53effd99f103b5bdb8a76 Mon Sep 17 00:00:00 2001 From: betegon Date: Fri, 13 Mar 2026 17:48:53 +0100 Subject: [PATCH 4/5] feat(dsn): expand MONOREPO_ROOTS with additional directory patterns Add projects, plugins, sites, workers, and functions to the list of recognized monorepo root directories. These cover Google-style monorepos, WordPress/Grafana plugin repos, multi-site setups, Cloudflare Workers, and AWS Lambda serverless monorepos respectively. Co-Authored-By: Claude Opus 4.6 --- src/lib/dsn/types.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib/dsn/types.ts b/src/lib/dsn/types.ts index 200ad855..cdbb2362 100644 --- a/src/lib/dsn/types.ts +++ b/src/lib/dsn/types.ts @@ -136,4 +136,9 @@ export const MONOREPO_ROOTS = [ "libs", "services", "modules", + "projects", + "plugins", + "sites", + "workers", + "functions", ] as const; From d37f40bdf59c511db3d223c16ab85dbde1b774b5 Mon Sep 17 00:00:00 2001 From: betegon Date: Fri, 13 Mar 2026 18:01:45 +0100 Subject: [PATCH 5/5] test(dsn): add deep nesting and depth-limit regression tests Add test for deeply nested monorepo files (depth 5 from root, the specific case from the original bug) and a negative test confirming non-monorepo directories still respect the depth limit. Co-Authored-By: Claude Opus 4.6 --- test/lib/dsn/code-scanner.test.ts | 34 +++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/lib/dsn/code-scanner.test.ts b/test/lib/dsn/code-scanner.test.ts index 4bfdc5c5..ef694a9c 100644 --- a/test/lib/dsn/code-scanner.test.ts +++ b/test/lib/dsn/code-scanner.test.ts @@ -462,6 +462,40 @@ describe("Code Scanner", () => { expect(beResult?.packagePath).toBe("packages/backend"); }); + test("finds DSNs deeply nested in monorepo packages", async () => { + // packages/spotlight/src/electron/main/index.ts is depth 5 from root, + // but after monorepo reset at packages/spotlight/, it's depth 3 — + // exactly at MAX_SCAN_DEPTH. This was a specific failing case. + mkdirSync(join(testDir, "packages/spotlight/src/electron/main"), { + recursive: true, + }); + writeFileSync( + join(testDir, "packages/spotlight/src/electron/main/index.ts"), + 'Sentry.init({ dsn: "https://electron@o123.ingest.sentry.io/333" });' + ); + + const result = await scanCodeForDsns(testDir); + expect(result.dsns).toHaveLength(1); + expect(result.dsns[0]?.raw).toBe( + "https://electron@o123.ingest.sentry.io/333" + ); + expect(result.dsns[0]?.packagePath).toBe("packages/spotlight"); + }); + + test("respects depth limit for non-monorepo directories", async () => { + // src/very/deeply/nested/config.ts is depth 4 — beyond MAX_SCAN_DEPTH (3). + // Should NOT be found. This confirms the depth reset only applies to + // monorepo package directories, not arbitrary subdirectories. + mkdirSync(join(testDir, "src/very/deeply/nested"), { recursive: true }); + writeFileSync( + join(testDir, "src/very/deeply/nested/config.ts"), + 'const DSN = "https://deep@o123.ingest.sentry.io/999";' + ); + + const result = await scanCodeForDsns(testDir); + expect(result.dsns).toEqual([]); + }); + test("gracefully handles unreadable files", async () => { const { chmodSync } = require("node:fs") as typeof import("node:fs"); const filePath = join(testDir, "secret.ts");