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)` 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/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/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/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; 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"); diff --git a/test/lib/dsn/code-scanner.test.ts b/test/lib/dsn/code-scanner.test.ts index 5d07aa57..ef694a9c 100644 --- a/test/lib/dsn/code-scanner.test.ts +++ b/test/lib/dsn/code-scanner.test.ts @@ -418,6 +418,84 @@ 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("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");