Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 | 45x 43x 43x 14x 14x 6x 3x 3x 3x 37x 37x 12x 37x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 23x 14x 13x 13x 23x 23x 1x 23x 23x 23x 23x 12x 23x 23x 23x 1x 23x 23x 23x 23x 16x 23x 23x 23x 7x 3x 3x 1x 2x 2x 2x 2x 1x 1x 11x 7x 4x 4x 4x 1x 3x 3x 3x 2x 1x 1x 14x 3x 11x 4x 4x 11x 3x 3x 37x 37x 3x 34x 34x 5x 29x 29x 4x 25x 19x 7x 5x 6x 1x | /**
* DSN Detector
*
* Detects Sentry DSN with GitHub CLI-style caching and project root detection.
*
* Detection algorithm:
* 1. Find project root by walking up from cwd (checks .env for DSN at each level)
* 2. If DSN found during walk-up, return immediately (fast path)
* 3. Check cache for project root
* 4. Full scan from project root with depth limiting
*
* Priority: .env with SENTRY_DSN > code > .env files > SENTRY_DSN env var
*/
import { readFile, stat } from "node:fs/promises";
import { join } from "node:path";
import {
getCachedDetection,
getCachedDsn,
setCachedDetection,
setCachedDsn,
} from "../db/dsn-cache.js";
import {
getCachedProjectRoot,
setCachedProjectRoot,
} from "../db/project-root-cache.js";
import {
extractFirstDsnFromContent,
scanCodeForDsns,
scanCodeForFirstDsn,
} from "./code-scanner.js";
import { detectFromEnv, SENTRY_DSN_ENV } from "./env.js";
import {
detectFromAllEnvFiles,
detectFromEnvFiles,
extractDsnFromEnvContent,
} from "./env-file.js";
import { isRegularFile } from "./fs-utils.js";
import { createDetectedDsn, createDsnFingerprint, parseDsn } from "./parser.js";
import { findProjectRoot } from "./project-root.js";
import type {
CachedDsnEntry,
DetectedDsn,
DsnDetectionResult,
DsnSource,
} from "./types.js";
/**
* Detect DSN with project root detection and caching support.
*
* Algorithm:
* 1. Find project root by walking up from cwd (checks .env for SENTRY_DSN to stop walk)
* 2. Check cache for project root (fast path)
* 3. Full scan from project root with depth limiting (slow path)
*
* Priority: code > .env files > SENTRY_DSN env var
*
* Note: Finding a DSN in .env during walk-up stops the walk (determines project root)
* but does NOT short-circuit detection - we still scan for code DSNs which have
* higher priority.
*
* @param cwd - Directory to start searching from
* @returns Detected DSN with source info, or null if not found
*/
export async function detectDsn(cwd: string): Promise<DetectedDsn | null> {
// 1. Find project root (may find DSN in .env along the way, but we don't
// return it immediately - code DSNs take priority)
const { projectRoot } = await findProjectRoot(cwd);
// 2. Check cache for project root (fast path)
const cached = getCachedDsn(projectRoot);
if (cached) {
const verified = await verifyCachedDsn(projectRoot, cached);
if (verified) {
// Check if DSN changed
if (verified.raw !== cached.dsn) {
// DSN changed - update cache
setCachedDsn(projectRoot, {
dsn: verified.raw,
projectId: verified.projectId,
orgId: verified.orgId,
source: verified.source,
sourcePath: verified.sourcePath,
});
return verified;
}
// Cache hit! Return with resolved info if available
return {
...verified,
resolved: cached.resolved,
};
}
// Cache invalid, fall through to full scan
}
// 3. Full scan from project root (slow path)
const detected = await fullScanFirst(projectRoot);
if (detected) {
// Cache for next time (without resolved info yet)
setCachedDsn(projectRoot, {
dsn: detected.raw,
projectId: detected.projectId,
orgId: detected.orgId,
source: detected.source,
sourcePath: detected.sourcePath,
});
}
return detected;
}
/**
* Detect all DSNs in a directory (supports monorepos)
*
* Unlike detectDsn, this finds ALL DSNs from all sources.
* Uses SQLite caching with mtime-based validation for fast repeated lookups.
*
* Algorithm:
* 1. Try cached project root, or walk up to find it
* 2. Try cached detection result (validates mtimes)
* 3. Full scan if cache miss, then store in cache
*
* Collection order matches priority: code > .env files > env var
*
* @param cwd - Directory to search in
* @returns Detection result with all found DSNs, fingerprint, and hasMultiple flag
*/
export async function detectAllDsns(cwd: string): Promise<DsnDetectionResult> {
// 1. Get project root (cached or walk-up)
let projectRoot: string;
const cachedRoot = await getCachedProjectRoot(cwd);
Iif (cachedRoot) {
projectRoot = cachedRoot.projectRoot;
} else {
const rootResult = await findProjectRoot(cwd);
projectRoot = rootResult.projectRoot;
// Cache the project root lookup
await setCachedProjectRoot(cwd, {
projectRoot: rootResult.projectRoot,
reason: rootResult.reason,
});
}
// 2. Try cached detection result
const cachedDetection = await getCachedDetection(projectRoot);
Iif (cachedDetection) {
// Cache hit! Return cached result
return {
primary: cachedDetection.allDsns[0] ?? null,
all: cachedDetection.allDsns,
hasMultiple: cachedDetection.allDsns.length > 1,
fingerprint: cachedDetection.fingerprint,
};
}
// 3. Full scan (cache miss)
const allDsns: DetectedDsn[] = [];
const seenRawDsns = new Set<string>();
const allSourceMtimes: Record<string, number> = {};
const allDirMtimes: Record<string, number> = {};
// Helper to add DSN if not duplicate
const addDsn = (dsn: DetectedDsn) => {
if (!seenRawDsns.has(dsn.raw)) {
allDsns.push(dsn);
seenRawDsns.add(dsn.raw);
}
};
// 3a. Check all code files from project root (highest priority)
const {
dsns: codeDsns,
sourceMtimes: codeMtimes,
dirMtimes: codeDirMtimes,
} = await scanCodeForDsns(projectRoot);
for (const dsn of codeDsns) {
addDsn(dsn);
}
Object.assign(allSourceMtimes, codeMtimes);
Object.assign(allDirMtimes, codeDirMtimes);
// 3b. Check all .env files from project root (includes monorepo packages/apps)
const { dsns: envFileDsns, sourceMtimes: envMtimes } =
await detectFromAllEnvFiles(projectRoot);
for (const dsn of envFileDsns) {
addDsn(dsn);
}
Object.assign(allSourceMtimes, envMtimes);
// 3c. Check env var (lowest priority) - no mtime for env vars
const envDsn = detectFromEnv();
if (envDsn) {
addDsn(envDsn);
}
// 4. Compute fingerprint and cache result
const fingerprint = createDsnFingerprint(allDsns);
// Get project root directory mtime for quick invalidation
// when files are added/removed at root level
let rootDirMtime = 0;
try {
const stats = await stat(projectRoot);
rootDirMtime = Math.floor(stats.mtimeMs);
} catch {
// Can't stat - still cache but validation will fail on next lookup
}
// Store in cache
setCachedDetection(projectRoot, {
fingerprint,
allDsns,
sourceMtimes: allSourceMtimes,
dirMtimes: allDirMtimes,
rootDirMtime,
});
// Multiple DSNs is valid in monorepos (different packages/apps)
const hasMultiple = allDsns.length > 1;
return {
primary: allDsns[0] ?? null,
all: allDsns,
hasMultiple,
fingerprint,
};
}
/**
* Check if a higher-priority code DSN exists.
* Used to invalidate low-priority cached DSNs when code is added.
*/
function checkForHigherPriorityCodeDsn(
cwd: string
): Promise<DetectedDsn | null> {
return scanCodeForFirstDsn(cwd);
}
/**
* Verify cached env var DSN is still valid.
* Also checks for higher-priority code and env_file DSNs.
*
* Priority: code > env_file > env_var
*/
async function verifyEnvVarCache(
cwd: string,
cached: CachedDsnEntry
): Promise<DetectedDsn | null> {
// First check if a code DSN exists (highest priority)
const codeDsn = await checkForHigherPriorityCodeDsn(cwd);
if (codeDsn) {
return codeDsn;
}
// Check for env_file DSN (medium priority)
const envFileDsn = await detectFromEnvFiles(cwd);
Iif (envFileDsn) {
return envFileDsn;
}
// No code or env_file DSN, verify the env var is still set
const envDsn = detectFromEnv();
if (envDsn?.raw === cached.dsn) {
return envDsn; // Same DSN - cache valid
}
return envDsn; // DSN changed or removed - return new value (may be null)
}
/**
* Verify cached file-based DSN is still valid.
*/
async function verifyFileDsnCache(
cwd: string,
cached: CachedDsnEntry
): Promise<DetectedDsn | null> {
if (!cached.sourcePath) {
return null;
}
const filePath = join(cwd, cached.sourcePath);
try {
// Guard: skip non-regular files (FIFOs, sockets, etc.) that would block.
// 1Password streams secrets via symlinked named pipes; Bun.file().text()
// blocks indefinitely on these.
if (!(await isRegularFile(filePath, "verifyFileDsnCache.stat"))) {
return null;
}
const content = await readFile(filePath, "utf-8");
const foundDsn = extractDsnFromContent(content, cached.source);
if (foundDsn === cached.dsn) {
return createDetectedDsn(cached.dsn, cached.source, cached.sourcePath);
}
Eif (foundDsn && parseDsn(foundDsn)) {
return createDetectedDsn(foundDsn, cached.source, cached.sourcePath);
}
} catch {
// File doesn't exist or can't read
}
return null;
}
/**
* Verify cached DSN is still valid.
*
* For low-priority sources (env, env_file), also checks if higher-priority
* code DSNs have been added since caching to maintain correct priority order.
*
* @param cwd - Directory
* @param cached - Cached DSN entry
* @returns Verified DSN or null if cache is invalid
*/
async function verifyCachedDsn(
cwd: string,
cached: CachedDsnEntry
): Promise<DetectedDsn | null> {
// Env var source (lowest priority) - check for higher-priority sources
if (cached.source === "env") {
return verifyEnvVarCache(cwd, cached);
}
// Env file source - check for higher-priority code DSNs first
if (cached.source === "env_file") {
const codeDsn = await checkForHigherPriorityCodeDsn(cwd);
Iif (codeDsn) {
return codeDsn;
}
}
// Verify file-based sources (code, env_file, config)
return verifyFileDsnCache(cwd, cached);
}
/**
* Extract DSN from content based on source type.
*
* @param content - File content
* @param source - Source type (env_file, code, etc.)
*/
function extractDsnFromContent(
content: string,
source: DsnSource
): string | null {
switch (source) {
case "env_file":
return extractDsnFromEnvContent(content);
case "code": {
// Use language-agnostic DSN extraction
return extractFirstDsnFromContent(content);
}
default:
return null;
}
}
/**
* Full scan to find first DSN (cache miss path)
*
* Searches in priority order:
* 1. Source code (explicit DSN takes highest priority)
* 2. .env files
* 3. SENTRY_DSN environment variable (lowest priority)
*/
async function fullScanFirst(cwd: string): Promise<DetectedDsn | null> {
// 1. Search source code first (explicit DSN = highest priority)
const codeDsn = await scanCodeForFirstDsn(cwd);
if (codeDsn) {
return codeDsn;
}
// 2. Check .env files
const envFileDsn = await detectFromEnvFiles(cwd);
if (envFileDsn) {
return envFileDsn;
}
// 3. Check SENTRY_DSN environment variable (lowest priority)
const envDsn = detectFromEnv();
if (envDsn) {
return envDsn;
}
return null;
}
/**
* Get a human-readable description of where DSN was found
*
* @param dsn - Detected DSN
* @returns Description string for display
*/
export function getDsnSourceDescription(dsn: DetectedDsn): string {
switch (dsn.source) {
case "env":
return `${dsn.sourcePath ?? SENTRY_DSN_ENV} environment variable`;
case "env_file":
return dsn.sourcePath ?? ".env file";
case "config":
return dsn.sourcePath ?? "config file";
case "code":
return dsn.sourcePath ?? "source code";
case "inferred":
return "directory name inference";
default:
return "unknown source";
}
}
|