Skip to content

Commit 25fe2f4

Browse files
authored
feat: support SENTRY_ORG and SENTRY_PROJECT environment variables (#280)
## Summary - Adds `SENTRY_ORG` and `SENTRY_PROJECT` environment variable support to the context resolution cascade, between CLI flags (highest priority) and config defaults - `SENTRY_PROJECT` supports the `<org>/<project>` combo notation (slash detection) matching the CLI native positional arg format - Updates the `ContextError` message to mention `SENTRY_ORG`/`SENTRY_PROJECT` as alternatives alongside `SENTRY_DSN` ## Motivation **CLI-17** (110 events, 50 users) is the highest-volume user error in the 0.11.0 release — users running commands like `sentry issue list` or `sentry event view <id>` without org/project context. Many are in CI environments or AI agents where DSN auto-detection cannot work. Legacy `sentry-cli` supported `SENTRY_ORG`/`SENTRY_PROJECT` and many users expect it. ## Changes ### `src/lib/resolve-target.ts` - New `resolveFromEnvVars()` helper reads `SENTRY_ORG` and `SENTRY_PROJECT`, handles `/` combo notation - Injected as step 2 in all four resolution functions: `resolveAllTargets`, `resolveOrgAndProject`, `resolveOrg`, `resolveOrgsForListing` - Resolution priority: CLI flags > env vars > config defaults > DSN detection > dir name inference - Refactored dir-name inference block to reduce cognitive complexity ### `src/lib/errors.ts` - Updated `DEFAULT_CONTEXT_ALTERNATIVES` to mention `SENTRY_ORG and SENTRY_PROJECT (or SENTRY_DSN)` ### `test/isolated/resolve-target.test.ts` - 16 new tests covering: separate env vars, combo notation, CLI flag priority, config default priority, org-only fallthrough, whitespace handling, edge cases ### `test/preload.ts` - Added `SENTRY_ORG` and `SENTRY_PROJECT` to env var cleanup Fixes CLI-17
1 parent 22d5f39 commit 25fe2f4

File tree

6 files changed

+515
-38
lines changed

6 files changed

+515
-38
lines changed

src/lib/errors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ export class ConfigError extends CliError {
132132

133133
const DEFAULT_CONTEXT_ALTERNATIVES = [
134134
"Run from a directory with a Sentry-configured project",
135-
"Set SENTRY_DSN environment variable",
135+
"Set SENTRY_ORG and SENTRY_PROJECT (or SENTRY_DSN) environment variables",
136136
] as const;
137137

138138
/**

src/lib/resolve-target.ts

Lines changed: 125 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
* Target Resolution
33
*
44
* Shared utilities for resolving organization and project context from
5-
* various sources: CLI flags, config defaults, and DSN detection.
5+
* various sources: CLI flags, environment variables, config defaults,
6+
* and DSN detection.
67
*
78
* Resolution priority (highest to lowest):
89
* 1. Explicit CLI flags
9-
* 2. Config defaults
10-
* 3. DSN auto-detection (source code, .env files, environment variables)
11-
* 4. Directory name inference (matches project slugs with word boundaries)
10+
* 2. SENTRY_ORG / SENTRY_PROJECT environment variables
11+
* 3. Config defaults
12+
* 4. DSN auto-detection (source code, .env files, environment variables)
13+
* 5. Directory name inference (matches project slugs with word boundaries)
1214
*/
1315

1416
import { basename } from "node:path";
@@ -461,6 +463,55 @@ async function inferFromDirectoryName(cwd: string): Promise<ResolvedTargets> {
461463
};
462464
}
463465

466+
/**
467+
* Read org/project from SENTRY_ORG and SENTRY_PROJECT environment variables.
468+
*
469+
* SENTRY_PROJECT supports the `<org>/<project>` combo notation (presence of
470+
* `/` distinguishes it from a plain project slug). When the combo form is
471+
* used, SENTRY_ORG is ignored.
472+
*
473+
* @returns Resolved org+project, org-only, or null if no env vars are set
474+
*/
475+
function resolveFromEnvVars(): {
476+
org: string;
477+
project?: string;
478+
detectedFrom: string;
479+
} | null {
480+
const rawProject = process.env.SENTRY_PROJECT?.trim();
481+
482+
// SENTRY_PROJECT=org/project combo takes priority.
483+
// If the value contains a slash it is always treated as combo notation;
484+
// a malformed combo (empty org or project part) is discarded entirely
485+
// so it cannot leak a slash into a project slug.
486+
if (rawProject?.includes("/")) {
487+
const slashIdx = rawProject.indexOf("/");
488+
const org = rawProject.slice(0, slashIdx);
489+
const project = rawProject.slice(slashIdx + 1);
490+
if (org && project) {
491+
return { org, project, detectedFrom: "SENTRY_PROJECT env var" };
492+
}
493+
// Malformed combo — fall through without using rawProject as a slug
494+
const envOrg = process.env.SENTRY_ORG?.trim();
495+
return envOrg ? { org: envOrg, detectedFrom: "SENTRY_ORG env var" } : null;
496+
}
497+
498+
const envOrg = process.env.SENTRY_ORG?.trim();
499+
500+
if (envOrg && rawProject) {
501+
return {
502+
org: envOrg,
503+
project: rawProject,
504+
detectedFrom: "SENTRY_ORG / SENTRY_PROJECT env vars",
505+
};
506+
}
507+
508+
if (envOrg) {
509+
return { org: envOrg, detectedFrom: "SENTRY_ORG env var" };
510+
}
511+
512+
return null;
513+
}
514+
464515
/**
465516
* Resolve all targets for monorepo-aware commands.
466517
*
@@ -469,9 +520,10 @@ async function inferFromDirectoryName(cwd: string): Promise<ResolvedTargets> {
469520
*
470521
* Resolution priority:
471522
* 1. Explicit org and project - returns single target
472-
* 2. Config defaults - returns single target
473-
* 3. DSN auto-detection - may return multiple targets
474-
* 4. Directory name inference - matches project slugs with word boundaries
523+
* 2. SENTRY_ORG / SENTRY_PROJECT env vars - returns single target
524+
* 3. Config defaults - returns single target
525+
* 4. DSN auto-detection - may return multiple targets
526+
* 5. Directory name inference - matches project slugs with word boundaries
475527
*
476528
* @param options - Resolution options with org, project, and cwd
477529
* @returns All resolved targets and optional footer message
@@ -504,7 +556,23 @@ export async function resolveAllTargets(
504556
);
505557
}
506558

507-
// 2. Config defaults
559+
// 2. SENTRY_ORG / SENTRY_PROJECT environment variables
560+
const envVars = resolveFromEnvVars();
561+
if (envVars?.project) {
562+
return {
563+
targets: [
564+
{
565+
org: envVars.org,
566+
project: envVars.project,
567+
orgDisplay: envVars.org,
568+
projectDisplay: envVars.project,
569+
detectedFrom: envVars.detectedFrom,
570+
},
571+
],
572+
};
573+
}
574+
575+
// 3. Config defaults
508576
const defaultOrg = await getDefaultOrganization();
509577
const defaultProject = await getDefaultProject();
510578
if (defaultOrg && defaultProject) {
@@ -520,11 +588,11 @@ export async function resolveAllTargets(
520588
};
521589
}
522590

523-
// 3. DSN auto-detection (may find multiple in monorepos)
591+
// 4. DSN auto-detection (may find multiple in monorepos)
524592
const detection = await detectAllDsns(cwd);
525593

526594
if (detection.all.length === 0) {
527-
// 4. Fallback: infer from directory name
595+
// 5. Fallback: infer from directory name
528596
return inferFromDirectoryName(cwd);
529597
}
530598

@@ -576,9 +644,10 @@ export async function resolveAllTargets(
576644
*
577645
* Resolution priority:
578646
* 1. Explicit org and project - both must be provided together
579-
* 2. Config defaults
580-
* 3. DSN auto-detection
581-
* 4. Directory name inference - matches project slugs with word boundaries
647+
* 2. SENTRY_ORG / SENTRY_PROJECT env vars
648+
* 3. Config defaults
649+
* 4. DSN auto-detection
650+
* 5. Directory name inference - matches project slugs with word boundaries
582651
*
583652
* @param options - Resolution options with org, project, and cwd
584653
* @returns Resolved target, or null if resolution failed
@@ -607,7 +676,19 @@ export async function resolveOrgAndProject(
607676
);
608677
}
609678

610-
// 2. Config defaults
679+
// 2. SENTRY_ORG / SENTRY_PROJECT environment variables
680+
const envVars = resolveFromEnvVars();
681+
if (envVars?.project) {
682+
return {
683+
org: envVars.org,
684+
project: envVars.project,
685+
orgDisplay: envVars.org,
686+
projectDisplay: envVars.project,
687+
detectedFrom: envVars.detectedFrom,
688+
};
689+
}
690+
691+
// 3. Config defaults
611692
const defaultOrg = await getDefaultOrganization();
612693
const defaultProject = await getDefaultProject();
613694
if (defaultOrg && defaultProject) {
@@ -619,7 +700,7 @@ export async function resolveOrgAndProject(
619700
};
620701
}
621702

622-
// 3. DSN auto-detection
703+
// 4. DSN auto-detection
623704
try {
624705
const dsnResult = await resolveFromDsn(cwd);
625706
if (dsnResult) {
@@ -629,32 +710,31 @@ export async function resolveOrgAndProject(
629710
// Fall through to directory inference
630711
}
631712

632-
// 4. Fallback: infer from directory name
713+
// 5. Fallback: infer from directory name
633714
const inferred = await inferFromDirectoryName(cwd);
634-
if (inferred.targets.length > 0) {
635-
const [first] = inferred.targets;
636-
if (first) {
637-
// If multiple matches, note it in detectedFrom
638-
return {
639-
...first,
640-
detectedFrom:
641-
inferred.targets.length > 1
642-
? `${first.detectedFrom} (1 of ${inferred.targets.length} matches)`
643-
: first.detectedFrom,
644-
};
645-
}
715+
const [first] = inferred.targets;
716+
if (!first) {
717+
return null;
646718
}
647719

648-
return null;
720+
// If multiple matches, note it in detectedFrom
721+
return {
722+
...first,
723+
detectedFrom:
724+
inferred.targets.length > 1
725+
? `${first.detectedFrom} (1 of ${inferred.targets.length} matches)`
726+
: first.detectedFrom,
727+
};
649728
}
650729

651730
/**
652731
* Resolve organization only from multiple sources.
653732
*
654733
* Resolution priority:
655734
* 1. Positional argument
656-
* 2. Config defaults
657-
* 3. DSN auto-detection
735+
* 2. SENTRY_ORG / SENTRY_PROJECT env vars
736+
* 3. Config defaults
737+
* 4. DSN auto-detection
658738
*
659739
* @param options - Resolution options with flag and cwd
660740
* @returns Resolved org, or null if resolution failed
@@ -669,13 +749,19 @@ export async function resolveOrg(
669749
return { org };
670750
}
671751

672-
// 2. Config defaults
752+
// 2. SENTRY_ORG / SENTRY_PROJECT environment variables
753+
const envVars = resolveFromEnvVars();
754+
if (envVars) {
755+
return { org: envVars.org, detectedFrom: envVars.detectedFrom };
756+
}
757+
758+
// 3. Config defaults
673759
const defaultOrg = await getDefaultOrganization();
674760
if (defaultOrg) {
675761
return { org: defaultOrg };
676762
}
677763

678-
// 3. DSN auto-detection
764+
// 4. DSN auto-detection
679765
try {
680766
return await resolveOrgFromDsn(cwd);
681767
} catch {
@@ -756,6 +842,12 @@ export async function resolveOrgsForListing(
756842
return { orgs: [orgFlag] };
757843
}
758844

845+
// 2. SENTRY_ORG / SENTRY_PROJECT environment variables
846+
const envVars = resolveFromEnvVars();
847+
if (envVars) {
848+
return { orgs: [envVars.org] };
849+
}
850+
759851
const defaultOrg = await getDefaultOrganization();
760852
if (defaultOrg) {
761853
return { orgs: [defaultOrg] };

0 commit comments

Comments
 (0)