From 0080e45eefd1b4878c5e1c09f18a8e7eda35e2c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Beteg=C3=B3n?= Date: Tue, 22 Sep 2026 16:26:09 +0200 Subject: [PATCH 1/3] fix(dsn-cache): treat empty DSN rows as cache misses setCachedDetection() writes dsn="" when no DSNs are found. getCachedDsn() returned those rows as hits, so detectDsn() wasted work verifying a bogus entry. Co-authored-by: Cursor --- packages/cli/src/lib/db/dsn-cache.ts | 8 ++++++++ packages/cli/test/lib/db/dsn-cache.test.ts | 21 +++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/packages/cli/src/lib/db/dsn-cache.ts b/packages/cli/src/lib/db/dsn-cache.ts index c1ef07f661..37b46f1411 100644 --- a/packages/cli/src/lib/db/dsn-cache.ts +++ b/packages/cli/src/lib/db/dsn-cache.ts @@ -148,6 +148,14 @@ export function getCachedDsn(directory: string): CachedDsnEntry | undefined { return; } + // Rows written by setCachedDetection() with an empty allDsns array store + // dsn="" and project_id="". Treat these as cache misses for the single-DSN + // path — callers expect a usable DSN when the result is defined. + if (!row.dsn) { + recordCacheHit("dsn", false); + return; + } + recordCacheHit("dsn", true); touchCacheEntry("dsn_cache", "directory", directory); return rowToCachedDsnEntry(row); diff --git a/packages/cli/test/lib/db/dsn-cache.test.ts b/packages/cli/test/lib/db/dsn-cache.test.ts index d0045ec3a0..e80520cd20 100644 --- a/packages/cli/test/lib/db/dsn-cache.test.ts +++ b/packages/cli/test/lib/db/dsn-cache.test.ts @@ -64,6 +64,27 @@ describe("getCachedDsn", () => { expect(result?.source).toBe("env"); expect(result?.sourcePath).toBe(".env"); }); + + test("treats empty DSN rows from setCachedDetection as cache misses", async () => { + const { stat } = await import("node:fs/promises"); + const rootStats = await stat(testProjectDir); + const rootDirMtime = Math.floor(rootStats.mtimeMs); + + setCachedDetection(testProjectDir, { + fingerprint: "fp-empty", + allDsns: [], + sourceMtimes: {}, + dirMtimes: {}, + rootDirMtime, + }); + + // Single-DSN callers must not receive dsn="". + expect(getCachedDsn(testProjectDir)).toBeUndefined(); + + // Full-detection cache still records a valid empty scan so we don't rescan. + const detection = await getCachedDetection(testProjectDir); + expect(detection?.allDsns).toHaveLength(0); + }); }); describe("setCachedDsn", () => { From 669b5121043fc6ed1a31eb18cc936a7c24827315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Beteg=C3=B3n?= Date: Tue, 22 Sep 2026 16:26:10 +0200 Subject: [PATCH 2/3] fix(cli): report swallowed best-effort failures to Sentry Debug logs only show up with --verbose, which users never pass. Route cache, DSN, and per-region lookup failures through reportCliError so unexpected errors become issues. Org-resolution fallbacks still only log, because the command-boundary error is already captured. Co-authored-by: Cursor --- packages/cli/src/lib/api/projects.ts | 60 ++++++++++++++----- packages/cli/src/lib/error-reporting.ts | 10 +++- packages/cli/src/lib/region.ts | 20 +++++-- .../cli/test/lib/api-client.coverage.test.ts | 49 ++++++++++++++- .../test/lib/api-client.multiregion.test.ts | 34 +++++++++-- 5 files changed, 142 insertions(+), 31 deletions(-) diff --git a/packages/cli/src/lib/api/projects.ts b/packages/cli/src/lib/api/projects.ts index a0a9019052..e21068772f 100644 --- a/packages/cli/src/lib/api/projects.ts +++ b/packages/cli/src/lib/api/projects.ts @@ -27,7 +27,9 @@ import { setCachedProjectByDsnKey, } from "../db/project-cache.js"; import { getCachedOrganizations } from "../db/regions.js"; +import { reportCliError } from "../error-reporting.js"; import { type AuthGuardSuccess, withAuthGuard } from "../errors.js"; +import { logger } from "../logger.js"; import { getApiBaseUrl } from "../sentry-client.js"; import { buildProjectUrl } from "../sentry-urls.js"; import { isAllDigits } from "../utils.js"; @@ -46,6 +48,20 @@ import { } from "./infrastructure.js"; import { getUserRegions, listOrganizations } from "./organizations.js"; +const log = logger.withTag("api.projects"); + +/** + * Surface a swallowed best-effort failure without failing the caller. + * + * `log.debug` is for `--verbose` local diagnosis. {@link reportCliError} + * is what makes unexpected failures visible as Sentry issues for users + * who never pass `--verbose`. + */ +function reportBestEffortFailure(message: string, error: unknown): void { + log.debug(message, error); + reportCliError(error); +} + /** * List all projects in an organization. * Automatically paginates through all API pages to return the complete list. @@ -74,13 +90,15 @@ export async function listProjects(orgSlug: string): Promise { // Populate project cache for shell completions (best-effort). // Mirrors how listOrganizations() calls setOrgRegions(). - // biome-ignore lint/plugin: grandfathered silent catch — see #1531; drain by adding log.debug()/log.warn() or re-throwing. try { const orgs = getCachedOrganizations(); const orgName = orgs.find((o) => o.slug === orgSlug)?.name ?? orgSlug; cacheProjectsForOrg(orgSlug, orgName, allResults); - } catch { - // Cache population is best-effort — never fail the command + } catch (error) { + reportBestEffortFailure( + `Failed to cache projects for org '${orgSlug}'`, + error + ); } return allResults; @@ -192,7 +210,7 @@ export type CreatedProjectDetails = { /** * Seed both project caches after a successful creation. * - * Best-effort: cache failures are silently swallowed so they never break + * Best-effort: cache failures are reported to Sentry but never break * project creation. Called by both `createProjectWithDsn` (team-scoped) * and `createProjectWithAutoTeam` (org-scoped) to keep cache behaviour * consistent across both creation paths. @@ -202,17 +220,18 @@ function seedProjectCaches( project: SentryProject, dsn: string | null ): void { - // biome-ignore lint/plugin: grandfathered silent catch — see #1531; drain by adding log.debug()/log.warn() or re-throwing. try { const orgName = resolveOrgDisplayName(orgSlug, project.organization?.name); cacheProjectsForOrg(orgSlug, orgName, [ { id: project.id, slug: project.slug, name: project.name }, ]); - } catch { - // Best-effort — don't let cache failures break project creation + } catch (error) { + reportBestEffortFailure( + `Failed to seed project cache for '${project.slug}'`, + error + ); } if (dsn) { - // biome-ignore lint/plugin: grandfathered silent catch — see #1531; drain by adding log.debug()/log.warn() or re-throwing. try { const publicKey = extractPublicKeyFromDsn(dsn); if (publicKey) { @@ -224,8 +243,11 @@ function seedProjectCaches( projectId: project.id, }); } - } catch { - // Best-effort — don't let cache failures break project creation + } catch (error) { + reportBestEffortFailure( + `Failed to seed DSN key cache for '${project.slug}'`, + error + ); } } } @@ -532,7 +554,6 @@ export async function findProjectByDsnKey( const results = await Promise.all( regions.map((region) => limit(async () => { - // biome-ignore lint/plugin: grandfathered silent catch — see #1531; drain by adding log.debug()/log.warn() or re-throwing. try { // Same `?query=dsn:` escape hatch as above (see the region-fallback // branch) — internal search param, no typed SDK operation yet. @@ -542,7 +563,11 @@ export async function findProjectByDsnKey( { params: { query: `dsn:${publicKey}` } } ); return data; - } catch { + } catch (error) { + reportBestEffortFailure( + `DSN key lookup failed in region '${region.url}'`, + error + ); return []; } }) @@ -649,8 +674,8 @@ export async function getProjectKeys( * Fetch the primary DSN for a project. * Returns the public DSN of the first active key, or null on any error. * - * Best-effort: failures are silently swallowed so callers can treat - * DSN display as optional (e.g., after project creation or in views). + * Best-effort: failures are reported to Sentry but callers can treat DSN + * display as optional (e.g., after project creation or in views). * * @param orgSlug - Organization slug * @param projectSlug - Project slug @@ -660,12 +685,15 @@ export async function tryGetPrimaryDsn( orgSlug: string, projectSlug: string ): Promise { - // biome-ignore lint/plugin: grandfathered silent catch — see #1531; drain by adding log.debug()/log.warn() or re-throwing. try { const keys = await getProjectKeys(orgSlug, projectSlug); const activeKey = keys.find((k) => k.isActive); return activeKey?.dsn.public ?? keys[0]?.dsn.public ?? null; - } catch { + } catch (error) { + reportBestEffortFailure( + `Failed to fetch DSN for '${orgSlug}/${projectSlug}'`, + error + ); return null; } } diff --git a/packages/cli/src/lib/error-reporting.ts b/packages/cli/src/lib/error-reporting.ts index 762459864e..62ed1bc27c 100644 --- a/packages/cli/src/lib/error-reporting.ts +++ b/packages/cli/src/lib/error-reporting.ts @@ -388,9 +388,15 @@ function setCliErrorContext(scope: Sentry.Scope, error: unknown): void { // --------------------------------------------------------------------------- /** - * Report a command-level error to Sentry. + * Report an error to Sentry without rethrowing. * - * - Silenced errors emit a metric and return without calling `captureException`. + * Call this at the command boundary for thrown failures, and in best-effort + * catch blocks that swallow the error so the command can continue. A debug + * log is not a substitute: users do not run `--verbose`, so swallowed + * unexpected failures must become Sentry issues. + * + * - Silenced errors (network, expected auth, 4xx) emit a metric and return + * without calling `captureException`. * - Captured errors get grouping tags + structured context on a fresh scope. */ export function reportCliError(error: unknown): void { diff --git a/packages/cli/src/lib/region.ts b/packages/cli/src/lib/region.ts index a79341829d..275fd4ac18 100644 --- a/packages/cli/src/lib/region.ts +++ b/packages/cli/src/lib/region.ts @@ -211,24 +211,32 @@ export async function resolveEffectiveOrg(orgSlug: string): Promise { // Normal slug: try a single resolveOrgRegion() call (1 API request) // instead of the heavy listOrganizationsUncached() fan-out (1+N requests). // If it succeeds, the slug is valid and the region is now cached. - // biome-ignore lint/plugin: grandfathered silent catch — see #1531; drain by adding log.debug()/log.warn() or re-throwing. try { await resolveOrgRegion(orgSlug); return orgSlug; - } catch { - // Org not found or auth error — fall through to return the original - // slug. The downstream API call will produce a relevant error. + } catch (error) { + // AuthError (and similar) — the downstream API call produces the + // user-facing error and reportCliError already runs at the command + // boundary. Log here so --verbose shows why we used the raw slug. + logger.debug( + `resolveOrgRegion failed for '${orgSlug}', using raw slug`, + error + ); return orgSlug; } } // DSN numeric ID: refresh the full org list to populate ID → slug mapping. // listOrganizationsUncached() populates org_regions with slug, region, org_id, and name. - // biome-ignore lint/plugin: grandfathered silent catch — see #1531; drain by adding log.debug()/log.warn() or re-throwing. try { const { listOrganizationsUncached } = await import("./api-client.js"); await listOrganizationsUncached(); - } catch { + } catch (error) { + // Same as above: the command fails downstream and is reported there. + logger.debug( + `Failed to refresh org list for numeric ID '${orgSlug}'`, + error + ); return orgSlug; } diff --git a/packages/cli/test/lib/api-client.coverage.test.ts b/packages/cli/test/lib/api-client.coverage.test.ts index 0f41a32f36..7c8f4b1161 100644 --- a/packages/cli/test/lib/api-client.coverage.test.ts +++ b/packages/cli/test/lib/api-client.coverage.test.ts @@ -6,8 +6,10 @@ * pattern as api-client.seer.test.ts. */ +// biome-ignore lint/performance/noNamespaceImport: needed for spyOn mocking +import * as Sentry from "@sentry/node-core/light"; import { number, object, string } from "valibot"; -import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { resolveEventInOrg } from "../../src/lib/api/events.js"; import { unwrapResult } from "../../src/lib/api/infrastructure.js"; import { @@ -964,8 +966,49 @@ describe("projects.ts", () => { }) ); - const dsn = await tryGetPrimaryDsn("test-org", "test-project"); - expect(dsn).toBeNull(); + const captureSpy = vi.spyOn(Sentry, "captureException"); + try { + const dsn = await tryGetPrimaryDsn("test-org", "test-project"); + expect(dsn).toBeNull(); + // 404 is expected user/API noise — silenced, not an issue. + expect(captureSpy).not.toHaveBeenCalled(); + } finally { + captureSpy.mockRestore(); + } + }); + + test("reports unexpected DSN fetch failures to Sentry", async () => { + globalThis.fetch = mockFetch( + async () => + new Response(JSON.stringify({ detail: "Internal error" }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }) + ); + + const captureSpy = vi.spyOn(Sentry, "captureException"); + const withScopeSpy = vi.spyOn(Sentry, "withScope"); + withScopeSpy.mockImplementation((fn: (scope: unknown) => void) => { + fn({ + setTag() { + /* noop */ + }, + setContext() { + /* noop */ + }, + setFingerprint() { + /* noop */ + }, + }); + }); + try { + const dsn = await tryGetPrimaryDsn("test-org", "test-project"); + expect(dsn).toBeNull(); + expect(captureSpy).toHaveBeenCalledTimes(1); + } finally { + captureSpy.mockRestore(); + withScopeSpy.mockRestore(); + } }); }); }); diff --git a/packages/cli/test/lib/api-client.multiregion.test.ts b/packages/cli/test/lib/api-client.multiregion.test.ts index 93686aae76..c9fe5785f1 100644 --- a/packages/cli/test/lib/api-client.multiregion.test.ts +++ b/packages/cli/test/lib/api-client.multiregion.test.ts @@ -5,7 +5,9 @@ * Covers region discovery, fan-out, and region-aware routing. */ -import { afterEach, beforeEach, describe, expect, test } from "vitest"; +// biome-ignore lint/performance/noNamespaceImport: needed for spyOn mocking +import * as Sentry from "@sentry/node-core/light"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { findProjectByDsnKey, getUserRegions, @@ -647,10 +649,34 @@ describe("findProjectByDsnKey (multi-region)", () => { ), }); - const project = await findProjectByDsnKey("abc123"); + const captureSpy = vi.spyOn(Sentry, "captureException"); + const withScopeSpy = vi.spyOn(Sentry, "withScope"); + withScopeSpy.mockImplementation((fn: (scope: unknown) => void) => { + fn({ + setTag() { + /* noop */ + }, + setContext() { + /* noop */ + }, + setFingerprint() { + /* noop */ + }, + }); + }); + try { + const project = await findProjectByDsnKey("abc123"); - // Should find project despite US region failing - expect(project?.slug).toBe("found-project"); + // Should find project despite US region failing + expect(project?.slug).toBe("found-project"); + expect(captureSpy).toHaveBeenCalledTimes(1); + expect(captureSpy.mock.calls[0]?.[0]).toMatchObject({ + message: "Network error", + }); + } finally { + captureSpy.mockRestore(); + withScopeSpy.mockRestore(); + } }); }); From 502602ed617ad923800c8a72a16ee16528234307 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Beteg=C3=B3n?= Date: Tue, 22 Sep 2026 18:22:54 +0200 Subject: [PATCH 3/3] test(cli): cover swallowed best-effort error reporting Patch coverage missed the listProjects/seedProjectCaches cache-failure catches and the resolveEffectiveOrg slug fallback. Co-authored-by: Cursor --- packages/cli/test/lib/api/projects.test.ts | 153 ++++++++++++++++-- .../test/lib/resolve-effective-org.test.ts | 35 +++- 2 files changed, 172 insertions(+), 16 deletions(-) diff --git a/packages/cli/test/lib/api/projects.test.ts b/packages/cli/test/lib/api/projects.test.ts index d372080b4f..9b7f12ffa5 100644 --- a/packages/cli/test/lib/api/projects.test.ts +++ b/packages/cli/test/lib/api/projects.test.ts @@ -7,8 +7,13 @@ * re-scan files and hit the API. */ -import { afterEach, beforeEach, describe, expect, test } from "vitest"; -import { createProjectWithDsn } from "../../../src/lib/api/projects.js"; +// biome-ignore lint/performance/noNamespaceImport: needed for spyOn mocking +import * as Sentry from "@sentry/node-core/light"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; +import { + createProjectWithDsn, + listProjects, +} from "../../../src/lib/api/projects.js"; import { setAuthToken } from "../../../src/lib/db/auth.js"; import { getCachedProjectByDsnKey, @@ -18,6 +23,44 @@ import { setOrgRegion } from "../../../src/lib/db/regions.js"; import type { SentryProject } from "../../../src/types/index.js"; import { mockFetch, useTestConfigDir } from "../../helpers.js"; +const projectCacheMocks = vi.hoisted(() => ({ + cacheProjectsForOrg: vi.fn(), + setCachedProjectByDsnKey: vi.fn(), + restoreActual: () => { + /* set in vi.mock factory */ + }, +})); + +vi.mock("../../../src/lib/db/project-cache.js", async (importOriginal) => { + const actual = + await importOriginal< + typeof import("../../../src/lib/db/project-cache.js") + >(); + projectCacheMocks.cacheProjectsForOrg.mockImplementation( + actual.cacheProjectsForOrg + ); + projectCacheMocks.setCachedProjectByDsnKey.mockImplementation( + actual.setCachedProjectByDsnKey + ); + projectCacheMocks.restoreActual = () => { + projectCacheMocks.cacheProjectsForOrg.mockImplementation( + actual.cacheProjectsForOrg + ); + projectCacheMocks.setCachedProjectByDsnKey.mockImplementation( + actual.setCachedProjectByDsnKey + ); + }; + return { + ...actual, + cacheProjectsForOrg: ( + ...args: Parameters + ) => projectCacheMocks.cacheProjectsForOrg(...args), + setCachedProjectByDsnKey: ( + ...args: Parameters + ) => projectCacheMocks.setCachedProjectByDsnKey(...args), + }; +}); + useTestConfigDir("api-projects-test-"); const SAMPLE_PROJECT: SentryProject = { @@ -56,12 +99,42 @@ beforeEach(async () => { originalFetch = globalThis.fetch; await setAuthToken("test-token"); setOrgRegion("test-org", "https://us.sentry.io"); + projectCacheMocks.restoreActual(); }); afterEach(() => { globalThis.fetch = originalFetch; + projectCacheMocks.restoreActual(); }); +function spyCaptureException(): { + captureSpy: ReturnType; + restore: () => void; +} { + const captureSpy = vi.spyOn(Sentry, "captureException"); + const withScopeSpy = vi.spyOn(Sentry, "withScope"); + withScopeSpy.mockImplementation((fn: (scope: unknown) => void) => { + fn({ + setTag() { + /* noop */ + }, + setContext() { + /* noop */ + }, + setFingerprint() { + /* noop */ + }, + }); + }); + return { + captureSpy, + restore() { + captureSpy.mockRestore(); + withScopeSpy.mockRestore(); + }, + }; +} + /** * Build a mock fetch that responds to the project-create POST and then * the client-keys GET that `tryGetPrimaryDsn` fires. @@ -206,22 +279,72 @@ describe("createProjectWithDsn", () => { expect(cached!.orgName).toBe("test-org"); }); - test("returns correct result even when cache write throws", async () => { - // This test verifies the try/catch around cache writes doesn't break - // the main creation flow. We test indirectly: if the function returns - // successfully, the try/catch is working (DB errors in cache-write - // paths don't propagate). + test("reports project-cache write failures to Sentry without failing creation", async () => { globalThis.fetch = mockCreateAndKeysFlow(); + projectCacheMocks.cacheProjectsForOrg.mockImplementation(() => { + throw new Error("disk full"); + }); + const { captureSpy, restore } = spyCaptureException(); + try { + const result = await createProjectWithDsn("test-org", "test-team", { + name: "My New Project", + }); + expect(result.project.id).toBe("42"); + expect(result.dsn).toBe(SAMPLE_DSN); + expect(captureSpy).toHaveBeenCalledTimes(1); + expect(captureSpy.mock.calls[0]?.[0]).toMatchObject({ + message: "disk full", + }); + } finally { + restore(); + } + }); - const result = await createProjectWithDsn("test-org", "test-team", { - name: "My New Project", + test("reports DSN-key cache write failures to Sentry without failing creation", async () => { + globalThis.fetch = mockCreateAndKeysFlow(); + projectCacheMocks.setCachedProjectByDsnKey.mockImplementation(() => { + throw new Error("dsn cache locked"); }); + const { captureSpy, restore } = spyCaptureException(); + try { + const result = await createProjectWithDsn("test-org", "test-team", { + name: "My New Project", + }); + expect(result.project.slug).toBe("my-new-project"); + expect(result.dsn).toBe(SAMPLE_DSN); + expect(captureSpy).toHaveBeenCalledTimes(1); + expect(captureSpy.mock.calls[0]?.[0]).toMatchObject({ + message: "dsn cache locked", + }); + } finally { + restore(); + } + }); +}); - // Primary result should always be returned - expect(result.project.id).toBe("42"); - expect(result.project.slug).toBe("my-new-project"); - expect(result.dsn).toBe(SAMPLE_DSN); - expect(result.url).toContain("test-org"); - expect(result.url).toContain("my-new-project"); +describe("listProjects cache seeding", () => { + test("reports cache write failures to Sentry without failing the list", async () => { + globalThis.fetch = mockFetch( + async () => + new Response(JSON.stringify([SAMPLE_PROJECT]), { + status: 200, + headers: { "Content-Type": "application/json" }, + }) + ); + projectCacheMocks.cacheProjectsForOrg.mockImplementation(() => { + throw new Error("disk full"); + }); + const { captureSpy, restore } = spyCaptureException(); + try { + const result = await listProjects("test-org"); + expect(result).toHaveLength(1); + expect(result[0]?.slug).toBe("my-new-project"); + expect(captureSpy).toHaveBeenCalledTimes(1); + expect(captureSpy.mock.calls[0]?.[0]).toMatchObject({ + message: "disk full", + }); + } finally { + restore(); + } }); }); diff --git a/packages/cli/test/lib/resolve-effective-org.test.ts b/packages/cli/test/lib/resolve-effective-org.test.ts index 6a7ac597bd..467710be5c 100644 --- a/packages/cli/test/lib/resolve-effective-org.test.ts +++ b/packages/cli/test/lib/resolve-effective-org.test.ts @@ -6,7 +6,7 @@ * org_regions cache. */ -import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { setAuthToken } from "../../src/lib/db/auth.js"; import { getOrgByNumericId, @@ -14,6 +14,7 @@ import { setOrgRegion, setOrgRegions, } from "../../src/lib/db/regions.js"; +import { logger } from "../../src/lib/logger.js"; import { resolveEffectiveOrg } from "../../src/lib/region.js"; import { mockFetch, useTestConfigDir } from "../helpers.js"; @@ -255,6 +256,38 @@ describe("resolveEffectiveOrg with API refresh", () => { expect(result).toBe("o1081365"); }); + test("logs when resolveOrgRegion throws for a slug", async () => { + const { clearAuth } = await import("../../src/lib/db/auth.js"); + await clearAuth(); + + const savedAuthToken = process.env.SENTRY_AUTH_TOKEN; + const savedSentryToken = process.env.SENTRY_TOKEN; + delete process.env.SENTRY_AUTH_TOKEN; + delete process.env.SENTRY_TOKEN; + + const debugSpy = vi.spyOn(logger, "debug"); + try { + const result = await resolveEffectiveOrg("missing-org"); + expect(result).toBe("missing-org"); + expect(debugSpy).toHaveBeenCalledWith( + "resolveOrgRegion failed for 'missing-org', using raw slug", + expect.anything() + ); + } finally { + debugSpy.mockRestore(); + if (savedAuthToken === undefined) { + delete process.env.SENTRY_AUTH_TOKEN; + } else { + process.env.SENTRY_AUTH_TOKEN = savedAuthToken; + } + if (savedSentryToken === undefined) { + delete process.env.SENTRY_TOKEN; + } else { + process.env.SENTRY_TOKEN = savedSentryToken; + } + } + }); + test("passes through non-DSN slugs starting with o", async () => { mockListOrgsApi("organic", "100", "https://us.sentry.io");