Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,11 @@ When creating your Sentry OAuth application:

## Environment Variables

| Variable | Description | Default |
| ------------------ | ------------------------------------ | -------------------- |
| `SENTRY_CLIENT_ID` | Sentry OAuth app client ID | (required) |
| `SENTRY_URL` | Sentry instance URL (for self-hosted)| `https://sentry.io` |
| Variable | Description | Default |
| ------------------ | ----------------------------------------------------- | -------------------- |
| `SENTRY_CLIENT_ID` | Sentry OAuth app client ID | (required) |
| `SENTRY_HOST` | Sentry instance URL (for self-hosted, takes precedence) | `https://sentry.io` |
| `SENTRY_URL` | Alias for `SENTRY_HOST` | `https://sentry.io` |

## Building

Expand Down
10 changes: 8 additions & 2 deletions docs/src/content/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,22 @@ The Sentry CLI can be configured through environment variables and a local datab

## Environment Variables

### `SENTRY_URL`
### `SENTRY_HOST`

Base URL of your Sentry instance. **Only needed for [self-hosted Sentry](./self-hosted/).** SaaS users (sentry.io) should not set this.

```bash
export SENTRY_URL=https://sentry.example.com
export SENTRY_HOST=https://sentry.example.com
```

When set, all API requests (including OAuth login) are directed to this URL instead of `https://sentry.io`. The CLI also sets this automatically when you pass a self-hosted Sentry URL as a command argument.

`SENTRY_HOST` takes precedence over `SENTRY_URL`. Both work identically — use whichever you prefer.

### `SENTRY_URL`

Alias for `SENTRY_HOST`. If both are set, `SENTRY_HOST` takes precedence.

### `SENTRY_ORG`

Default organization slug. Skips organization auto-detection.
Expand Down
13 changes: 7 additions & 6 deletions docs/src/content/docs/self-hosted.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ title: Self-Hosted Sentry
description: Using the Sentry CLI with a self-hosted Sentry instance
---

The CLI works with self-hosted Sentry instances. Set the `SENTRY_URL` environment variable to point at your instance:
The CLI works with self-hosted Sentry instances. Set the `SENTRY_HOST` (or `SENTRY_URL`) environment variable to point at your instance:

```bash
export SENTRY_URL=https://sentry.example.com
export SENTRY_HOST=https://sentry.example.com
```

## Authenticating
Expand All @@ -27,14 +27,14 @@ The OAuth device flow requires **Sentry 26.1.0 or later** and a public OAuth app
Pass your instance URL and the client ID:

```bash
SENTRY_URL=https://sentry.example.com SENTRY_CLIENT_ID=your-client-id sentry auth login
SENTRY_HOST=https://sentry.example.com SENTRY_CLIENT_ID=your-client-id sentry auth login
```

:::tip
You can export both variables in your shell profile so every CLI invocation picks them up:

```bash
export SENTRY_URL=https://sentry.example.com
export SENTRY_HOST=https://sentry.example.com
export SENTRY_CLIENT_ID=your-client-id
```
:::
Expand All @@ -48,7 +48,7 @@ If your instance is on an older version or you prefer not to create an OAuth app
3. Pass it to the CLI:

```bash
SENTRY_URL=https://sentry.example.com sentry auth login --token YOUR_TOKEN
SENTRY_HOST=https://sentry.example.com sentry auth login --token YOUR_TOKEN
```

## After Login
Expand All @@ -66,7 +66,8 @@ If you pass a self-hosted Sentry URL as a command argument (e.g., an issue or ev

| Variable | Description |
|----------|-------------|
| `SENTRY_URL` | Base URL of your Sentry instance |
| `SENTRY_HOST` | Base URL of your Sentry instance (takes precedence over `SENTRY_URL`) |
| `SENTRY_URL` | Alias for `SENTRY_HOST` |
| `SENTRY_CLIENT_ID` | Client ID of your public OAuth application |
| `SENTRY_ORG` | Default organization slug |
| `SENTRY_PROJECT` | Default project slug (supports `org/project` format) |
Expand Down
8 changes: 8 additions & 0 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ export const DEFAULT_SENTRY_HOST = "sentry.io";
/** Default Sentry SaaS URL (control silo for OAuth and region discovery) */
export const DEFAULT_SENTRY_URL = `https://${DEFAULT_SENTRY_HOST}`;

/**
* Resolve the Sentry instance URL from environment variables.
* Checks SENTRY_HOST first, then SENTRY_URL, then falls back to undefined.
*/
export function getConfiguredSentryUrl(): string | undefined {
return process.env.SENTRY_HOST ?? process.env.SENTRY_URL;
}
Comment thread
cursor[bot] marked this conversation as resolved.

/** CLI version string, available for help output and other uses */
export const CLI_VERSION =
typeof SENTRY_CLI_VERSION !== "undefined" ? SENTRY_CLI_VERSION : "0.0.0-dev";
Expand Down
10 changes: 5 additions & 5 deletions src/lib/dsn/code-scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import path from "node:path";
import * as Sentry from "@sentry/bun";
import ignore, { type Ignore } from "ignore";
import pLimit from "p-limit";
import { DEFAULT_SENTRY_HOST } from "../constants.js";
import { DEFAULT_SENTRY_HOST, getConfiguredSentryUrl } from "../constants.js";
import { ConfigError } from "../errors.js";
import { logger } from "../logger.js";
import { withTracingSpan } from "../telemetry.js";
Expand Down Expand Up @@ -316,18 +316,18 @@ function isCommentedLine(trimmedLine: string): boolean {
* @returns The expected host domain for DSN validation
*/
function getExpectedHost(): string {
const sentryUrl = process.env.SENTRY_URL;
const sentryUrl = getConfiguredSentryUrl();

if (sentryUrl) {
// Self-hosted: only accept DSNs matching the configured host
try {
const url = new URL(sentryUrl);
return url.host;
} catch {
// Invalid SENTRY_URL - throw immediately since nothing will work
// Invalid SENTRY_HOST/SENTRY_URL - throw immediately since nothing will work
throw new ConfigError(
`SENTRY_URL "${sentryUrl}" is not a valid URL`,
"Set SENTRY_URL to a valid URL (e.g., https://sentry.example.com) or unset it to use sentry.io"
`SENTRY_HOST/SENTRY_URL "${sentryUrl}" is not a valid URL`,
"Set SENTRY_HOST/SENTRY_URL to a valid URL (e.g., https://sentry.example.com) or unset it to use sentry.io"
);
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/lib/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
TokenErrorResponseSchema,
TokenResponseSchema,
} from "../types/index.js";
import { DEFAULT_SENTRY_URL, getConfiguredSentryUrl } from "./constants.js";
import { setAuthToken } from "./db/auth.js";
import { ApiError, AuthError, ConfigError, DeviceFlowError } from "./errors.js";
import { withHttpSpan } from "./telemetry.js";
Expand All @@ -23,7 +24,7 @@ import { withHttpSpan } from "./telemetry.js";
* by the device flow and token refresh.
*/
function getSentryUrl(): string {
return process.env.SENTRY_URL ?? "https://sentry.io";
return getConfiguredSentryUrl() ?? DEFAULT_SENTRY_URL;
}

/**
Expand Down
5 changes: 3 additions & 2 deletions src/lib/region.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import { retrieveAnOrganization } from "@sentry/api";
import { getConfiguredSentryUrl } from "./constants.js";
import { getOrgByNumericId, getOrgRegion, setOrgRegion } from "./db/regions.js";
import { stripDsnOrgPrefix } from "./dsn/index.js";
import { withAuthGuard } from "./errors.js";
Expand Down Expand Up @@ -66,8 +67,8 @@ export async function resolveOrgRegion(orgSlug: string): Promise<string> {
* Returns false for self-hosted instances that don't have regional URLs.
*/
export function isMultiRegionEnabled(): boolean {
// Self-hosted instances (custom SENTRY_URL) typically don't have multi-region
const baseUrl = process.env.SENTRY_URL;
// Self-hosted instances (custom SENTRY_HOST/SENTRY_URL) typically don't have multi-region
const baseUrl = getConfiguredSentryUrl();
if (baseUrl && !isSentrySaasUrl(baseUrl)) {
return false;
}
Expand Down
10 changes: 7 additions & 3 deletions src/lib/sentry-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
* through the SDK function options (baseUrl, fetch, headers).
*/

import { DEFAULT_SENTRY_URL, getUserAgent } from "./constants.js";
import {
DEFAULT_SENTRY_URL,
getConfiguredSentryUrl,
getUserAgent,
} from "./constants.js";
import { getAuthToken, isEnvTokenActive, refreshToken } from "./db/auth.js";
import { getCachedResponse, storeCachedResponse } from "./response-cache.js";
import { withHttpSpan } from "./telemetry.js";
Expand Down Expand Up @@ -391,7 +395,7 @@ function getAuthenticatedFetch(): typeof fetch {
* Supports self-hosted instances via SENTRY_URL env var.
*/
export function getApiBaseUrl(): string {
return process.env.SENTRY_URL || DEFAULT_SENTRY_URL;
return getConfiguredSentryUrl() ?? DEFAULT_SENTRY_URL;
}

/**
Expand All @@ -402,7 +406,7 @@ export function getApiBaseUrl(): string {
* (e.g., from URL argument parsing for self-hosted instances) is respected.
*/
export function getControlSiloUrl(): string {
return process.env.SENTRY_URL || DEFAULT_SENTRY_URL;
return getConfiguredSentryUrl() ?? DEFAULT_SENTRY_URL;
}

/**
Expand Down
5 changes: 4 additions & 1 deletion src/lib/sentry-url-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,13 @@ export function parseSentryUrl(input: string): ParsedSentryUrl | null {
export function applySentryUrlContext(baseUrl: string): void {
if (isSentrySaasUrl(baseUrl)) {
// Clear any self-hosted URL so API calls fall back to default SaaS routing.
// Without this, a stale SENTRY_URL would route SaaS requests to the wrong host.
// Without this, a stale SENTRY_HOST/SENTRY_URL would route SaaS requests to the wrong host.
// biome-ignore lint/performance/noDelete: process.env requires delete to truly unset; assignment coerces to string in Node.js
delete process.env.SENTRY_HOST;
// biome-ignore lint/performance/noDelete: process.env requires delete to truly unset; assignment coerces to string in Node.js
delete process.env.SENTRY_URL;
return;
}
process.env.SENTRY_HOST = baseUrl;
Comment thread
betegon marked this conversation as resolved.
process.env.SENTRY_URL = baseUrl;
}
8 changes: 6 additions & 2 deletions src/lib/sentry-urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@
* Supports self-hosted instances via SENTRY_URL environment variable.
*/

import { DEFAULT_SENTRY_HOST, DEFAULT_SENTRY_URL } from "./constants.js";
import {
DEFAULT_SENTRY_HOST,
DEFAULT_SENTRY_URL,
getConfiguredSentryUrl,
} from "./constants.js";

/**
* Get the Sentry web base URL.
* Supports self-hosted instances via SENTRY_URL env var.
*/
export function getSentryBaseUrl(): string {
return process.env.SENTRY_URL ?? DEFAULT_SENTRY_URL;
return getConfiguredSentryUrl() ?? DEFAULT_SENTRY_URL;
}

/**
Expand Down
24 changes: 23 additions & 1 deletion test/lib/dsn/code-scanner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ describe("Code Scanner", () => {

afterEach(() => {
rmSync(testDir, { recursive: true, force: true });
delete process.env.SENTRY_HOST;
delete process.env.SENTRY_URL;
});

Expand Down Expand Up @@ -211,9 +212,30 @@ describe("Code Scanner", () => {

// Invalid SENTRY_URL should throw immediately since nothing will work
expect(() => extractDsnsFromContent(content)).toThrow(
/SENTRY_URL.*not a valid URL/
/SENTRY_HOST\/SENTRY_URL.*not a valid URL/
);
});

test("accepts self-hosted DSNs when SENTRY_HOST is set", () => {
process.env.SENTRY_HOST = "https://sentry.mycompany.com:9000";
const content = `
const DSN = "https://abc@sentry.mycompany.com:9000/123";
`;
const dsns = extractDsnsFromContent(content);
expect(dsns).toEqual(["https://abc@sentry.mycompany.com:9000/123"]);
});

test("SENTRY_HOST takes precedence over SENTRY_URL for DSN validation", () => {
process.env.SENTRY_HOST = "https://sentry.mycompany.com:9000";
process.env.SENTRY_URL = "https://sentry.other.com";
const content = `
const DSN1 = "https://abc@sentry.mycompany.com:9000/123";
const DSN2 = "https://def@sentry.other.com/456";
`;
const dsns = extractDsnsFromContent(content);
// Only the SENTRY_HOST DSN should be accepted
expect(dsns).toEqual(["https://abc@sentry.mycompany.com:9000/123"]);
});
});

describe("extractFirstDsnFromContent", () => {
Expand Down
27 changes: 26 additions & 1 deletion test/lib/region.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,21 @@ import { useTestConfigDir } from "../helpers.js";
useTestConfigDir("region-resolve-");

beforeEach(async () => {
// Clear any SENTRY_URL override for most tests
// Clear any SENTRY_HOST/SENTRY_URL override for most tests
delete process.env.SENTRY_HOST;
delete process.env.SENTRY_URL;
// Set up auth token for API tests
await setAuthToken("test-token");
});

afterEach(() => {
delete process.env.SENTRY_HOST;
delete process.env.SENTRY_URL;
});

describe("getSentryBaseUrl", () => {
test("returns sentry.io by default", () => {
delete process.env.SENTRY_HOST;
delete process.env.SENTRY_URL;
expect(getSentryBaseUrl()).toBe("https://sentry.io");
});
Comment thread
sentry[bot] marked this conversation as resolved.
Expand All @@ -37,6 +40,17 @@ describe("getSentryBaseUrl", () => {
process.env.SENTRY_URL = "https://sentry.mycompany.com";
expect(getSentryBaseUrl()).toBe("https://sentry.mycompany.com");
});

test("respects SENTRY_HOST env var", () => {
process.env.SENTRY_HOST = "https://sentry.mycompany.com";
expect(getSentryBaseUrl()).toBe("https://sentry.mycompany.com");
});

test("SENTRY_HOST takes precedence over SENTRY_URL", () => {
process.env.SENTRY_HOST = "https://host.example.com";
process.env.SENTRY_URL = "https://url.example.com";
expect(getSentryBaseUrl()).toBe("https://host.example.com");
});
});

describe("isMultiRegionEnabled", () => {
Expand Down Expand Up @@ -85,6 +99,17 @@ describe("isMultiRegionEnabled", () => {
process.env.SENTRY_URL = "not-a-valid-url";
expect(isMultiRegionEnabled()).toBe(false);
});

test("respects SENTRY_HOST for self-hosted", () => {
process.env.SENTRY_HOST = "https://sentry.mycompany.com";
expect(isMultiRegionEnabled()).toBe(false);
});

test("SENTRY_HOST takes precedence over SENTRY_URL", () => {
process.env.SENTRY_HOST = "https://sentry.mycompany.com";
process.env.SENTRY_URL = "https://us.sentry.io";
expect(isMultiRegionEnabled()).toBe(false);
});
});

describe("resolveOrgRegion", () => {
Expand Down
Loading
Loading