Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions src/commands/span/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ async function handleTraceMode(
cursor,
...timeRangeToApiParams(timeRange),
extraFields: extraApiFields,
allProjects: true,
})
);

Expand Down
17 changes: 14 additions & 3 deletions src/lib/api/traces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,8 @@ type ListSpansOptions = {
start?: string;
/** Absolute end datetime (ISO-8601). Mutually exclusive with statsPeriod. */
end?: string;
/** When true, query spans across all projects (sends project=-1). Used for cross-project traces. */
allProjects?: boolean;
};

/**
Expand All @@ -402,8 +404,10 @@ export async function listSpans(
projectSlug: string,
options: ListSpansOptions = {}
): Promise<PaginatedResponse<SpanListItem[]>> {
const isNumericProject = isAllDigits(projectSlug);
const projectFilter = isNumericProject ? "" : `project:${projectSlug}`;
const useAllProjects = options.allProjects === true;
const isNumericProject = !useAllProjects && isAllDigits(projectSlug);
const projectFilter =
useAllProjects || isNumericProject ? "" : `project:${projectSlug}`;
const fullQuery = [projectFilter, options.query].filter(Boolean).join(" ");

const fields = options.extraFields?.length
Expand All @@ -412,14 +416,21 @@ export async function listSpans(

const regionUrl = await resolveOrgRegion(orgSlug);

let projectParam: string | number | undefined;
if (useAllProjects) {
projectParam = -1;
} else if (isNumericProject) {
projectParam = projectSlug;
}

const { data: response, headers } = await apiRequestToRegion<SpansResponse>(
regionUrl,
`/organizations/${orgSlug}/events/`,
{
params: {
dataset: "spans",
field: fields,
project: isNumericProject ? projectSlug : undefined,
project: projectParam,
query: fullQuery || undefined,
per_page: options.limit || 10,
statsPeriod:
Expand Down
14 changes: 10 additions & 4 deletions src/lib/dsn/env-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,17 @@ export const ENV_FILES = [
] as const;

/**
* Pattern to match SENTRY_DSN in .env files.
* Handles: SENTRY_DSN=value, SENTRY_DSN="value", SENTRY_DSN='value'
* Also handles trailing comments: SENTRY_DSN=value # comment
* Pattern to match Sentry DSN variables in .env files.
* Handles the canonical `SENTRY_DSN` as well as framework-prefixed variants
* commonly used by Next.js, Vite, CRA, Expo, Nuxt, and others:
* NEXT_PUBLIC_SENTRY_DSN, REACT_APP_SENTRY_DSN, VITE_SENTRY_DSN,
* EXPO_PUBLIC_SENTRY_DSN, NUXT_PUBLIC_SENTRY_DSN, etc.
*
* Handles: VAR=value, VAR="value", VAR='value'
* Also handles trailing comments: VAR=value # comment
*/
const ENV_DSN_PATTERN = /^SENTRY_DSN\s*=\s*(['"]?)(.+?)\1\s*(?:#.*)?$/;
const ENV_DSN_PATTERN =
/^(?:\w+_)?SENTRY_DSN\s*=\s*(['"]?)(.+?)\1\s*(?:#.*)?$/;

/**
* Extract SENTRY_DSN value from .env file content.
Expand Down
34 changes: 29 additions & 5 deletions src/lib/dsn/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,22 @@ import type { DetectedDsn } from "./types.js";
export const SENTRY_DSN_ENV = "SENTRY_DSN";

/**
* Detect DSN from environment variable.
* Framework-prefixed env var names that commonly hold a Sentry DSN.
* Checked in order after `SENTRY_DSN` (canonical name has priority).
*/
const FRAMEWORK_DSN_VARS = [
"NEXT_PUBLIC_SENTRY_DSN",
"REACT_APP_SENTRY_DSN",
"VITE_SENTRY_DSN",
"EXPO_PUBLIC_SENTRY_DSN",
"NUXT_PUBLIC_SENTRY_DSN",
] as const;

/**
* Detect DSN from environment variables.
*
* Checks `SENTRY_DSN` first (canonical), then common framework-prefixed
* variants (NEXT_PUBLIC_SENTRY_DSN, REACT_APP_SENTRY_DSN, etc.).
*
* @returns Detected DSN or null if not set/invalid
*
Expand All @@ -23,10 +38,19 @@ export const SENTRY_DSN_ENV = "SENTRY_DSN";
* // { raw: "...", source: "env", projectId: "456", ... }
*/
export function detectFromEnv(): DetectedDsn | null {
const dsn = getEnv()[SENTRY_DSN_ENV];
if (!dsn) {
return null;
const env = getEnv();

const canonical = env[SENTRY_DSN_ENV];
if (canonical) {
return createDetectedDsn(canonical, "env");
}

for (const varName of FRAMEWORK_DSN_VARS) {
const value = env[varName];
if (value) {
return createDetectedDsn(value, "env");
}
}

return createDetectedDsn(dsn, "env");
return null;
}
122 changes: 122 additions & 0 deletions src/lib/init/clack-plain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* Plain-mode adapter for @clack/prompts.
*
* When stdout is not a TTY (piped, redirected, CI), clack writes raw ANSI
* escape sequences (cursor hide/show, erase codes, spinner animation) that
* make captured output unreadable. This module re-exports the clack functions
* used by the init wizard, replacing them with plain-text equivalents when
* `isPlainOutput()` is true.
*
* Interactive prompts (confirm, select, multiselect) pass through to real
* clack unconditionally — they're only reached in TTY mode (the wizard
* guards with `process.stdin.isTTY` before prompts).
*/

import * as clack from "@clack/prompts";
import { isPlainOutput } from "../formatters/plain-detect.js";
import { stripAnsi } from "../formatters/plain-detect.js";

function plainWrite(message: string): void {
process.stdout.write(`${stripAnsi(message)}\n`);
}

function plainWriteErr(message: string): void {
process.stderr.write(`${stripAnsi(message)}\n`);
}

/** intro() — in plain mode, just print the title without box-drawing chars */
export function intro(title?: string): void {
if (isPlainOutput()) {
if (title) {
plainWrite(`── ${stripAnsi(String(title))} ──`);
}
return;
}
clack.intro(title);
}

/** outro() — in plain mode, print a simple closing line */
export function outro(message?: string): void {
if (isPlainOutput()) {
if (message) {
plainWrite(stripAnsi(String(message)));
}
return;
}
clack.outro(message);
}

/** cancel() — in plain mode, print the cancellation message */
export function cancel(message?: string): void {
if (isPlainOutput()) {
if (message) {
plainWriteErr(stripAnsi(String(message)));
}
return;
}
clack.cancel(message);
}

/**
* log — delegates to the real clack.log, but in plain mode strips ANSI
* and writes to stdout/stderr without box-drawing characters.
*/
export const log: typeof clack.log = {
message(message?: string, opts?: { symbol?: string }) {
if (isPlainOutput()) {
if (message) {
const prefix = opts?.symbol
? `${stripAnsi(opts.symbol)} `
: "";
plainWrite(`${prefix}${stripAnsi(message)}`);
}
return;
}
clack.log.message(message, opts);
},
info(message: string) {
if (isPlainOutput()) {
plainWrite(`INFO: ${stripAnsi(message)}`);
return;
}
clack.log.info(message);
},
success(message: string) {
if (isPlainOutput()) {
plainWrite(`OK: ${stripAnsi(message)}`);
return;
}
clack.log.success(message);
},
step(message: string) {
if (isPlainOutput()) {
plainWrite(`> ${stripAnsi(message)}`);
return;
}
clack.log.step(message);
},
warn(message: string) {
if (isPlainOutput()) {
plainWriteErr(`WARN: ${stripAnsi(message)}`);
return;
}
clack.log.warn(message);
},
warning(message: string) {
if (isPlainOutput()) {
plainWriteErr(`WARN: ${stripAnsi(message)}`);
return;
}
clack.log.warning(message);
},
error(message: string) {
if (isPlainOutput()) {
plainWriteErr(`ERROR: ${stripAnsi(message)}`);
return;
}
clack.log.error(message);
},
};

export { isCancel } from "@clack/prompts";
export { confirm, select, multiselect } from "@clack/prompts";
2 changes: 1 addition & 1 deletion src/lib/init/clack-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Shared helpers for the clack-based init wizard UI.
*/

import { cancel, isCancel } from "@clack/prompts";
import { cancel, isCancel } from "./clack-plain.js";
import { terminalLink } from "../formatters/colors.js";
import { SENTRY_DOCS_URL } from "./constants.js";

Expand Down
2 changes: 1 addition & 1 deletion src/lib/init/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Format wizard results and errors for terminal display using clack.
*/

import { cancel, log, outro } from "@clack/prompts";
import { cancel, log, outro } from "./clack-plain.js";
import { terminalLink } from "../formatters/colors.js";
import { colorTag, mdKvTable, renderMarkdown } from "../formatters/markdown.js";
import { featureLabel } from "./clack-utils.js";
Expand Down
2 changes: 1 addition & 1 deletion src/lib/init/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* `checkGitStatus` orchestrator (coupled to `@clack/prompts` UI).
*/

import { confirm, isCancel, log } from "@clack/prompts";
import { confirm, isCancel, log } from "./clack-plain.js";
import {
getUncommittedFiles,
isInsideGitWorkTree as isInsideWorkTree,
Expand Down
2 changes: 1 addition & 1 deletion src/lib/init/interactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Respects --yes flag for non-interactive mode.
*/

import { confirm, log, multiselect, select } from "@clack/prompts";
import { confirm, log, multiselect, select } from "./clack-plain.js";
import chalk from "chalk";
import {
abortIfCancelled,
Expand Down
2 changes: 1 addition & 1 deletion src/lib/init/preflight.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cancel, isCancel, log, select } from "@clack/prompts";
import { cancel, isCancel, log, select } from "./clack-plain.js";
import type { SentryTeam } from "../../types/index.js";
import { listOrganizations } from "../api-client.js";
import { getAuthToken } from "../db/auth.js";
Expand Down
8 changes: 5 additions & 3 deletions src/lib/init/spinner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
renderInlineMarkdown,
stripColorTags,
} from "../formatters/markdown.js";
import { isPlainOutput } from "../formatters/plain-detect.js";

const HIDE_CURSOR = "\u001B[?25l";
const SHOW_CURSOR = "\u001B[?25h";
Expand Down Expand Up @@ -108,11 +109,12 @@ export function createWizardSpinner(
running = true;
frameIndex = 0;
message = nextMessage;
if (output.isTTY) {
const plain = isPlainOutput();
if (output.isTTY && !plain) {
output.write(HIDE_CURSOR);
}
renderCurrentFrame();
if (output.isTTY) {
if (output.isTTY && !plain) {
timer = setInterval(() => {
frameIndex = (frameIndex + 1) % SPINNER_FRAMES.length;
renderCurrentFrame();
Expand All @@ -138,7 +140,7 @@ export function createWizardSpinner(
`${renderInlineMarkdown(formatStoppedBlock(message, code))}\n`
);
}
if (output.isTTY) {
if (output.isTTY && !isPlainOutput()) {
output.write(SHOW_CURSOR);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/init/wizard-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import { randomBytes } from "node:crypto";
import { basename } from "node:path";
import { cancel, confirm, intro, log } from "@clack/prompts";
import { cancel, confirm, intro, log } from "./clack-plain.js";
import { MastraClient } from "@mastra/client-js";
import { captureException, getTraceData } from "@sentry/node-core/light";
import { formatBanner } from "../banner.js";
Expand Down
9 changes: 9 additions & 0 deletions test/lib/init/formatters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ const noop = () => {
/* suppress clack output */
};

let savedPlainOutput: string | undefined;

beforeEach(() => {
savedPlainOutput = process.env.SENTRY_PLAIN_OUTPUT;
process.env.SENTRY_PLAIN_OUTPUT = "0";
logMessageSpy = spyOn(clack.log, "message").mockImplementation(noop);
outroSpy = spyOn(clack, "outro").mockImplementation(noop);
cancelSpy = spyOn(clack, "cancel").mockImplementation(noop);
Expand All @@ -39,6 +43,11 @@ afterEach(() => {
logInfoSpy.mockRestore();
logWarnSpy.mockRestore();
logErrorSpy.mockRestore();
if (savedPlainOutput === undefined) {
delete process.env.SENTRY_PLAIN_OUTPUT;
} else {
process.env.SENTRY_PLAIN_OUTPUT = savedPlainOutput;
}
});

describe("formatResult", () => {
Expand Down
9 changes: 9 additions & 0 deletions test/lib/init/git.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ let confirmSpy: ReturnType<typeof spyOn>;
let isCancelSpy: ReturnType<typeof spyOn>;
let logWarnSpy: ReturnType<typeof spyOn>;

let savedPlainOutput: string | undefined;

beforeEach(() => {
savedPlainOutput = process.env.SENTRY_PLAIN_OUTPUT;
process.env.SENTRY_PLAIN_OUTPUT = "0";
isInsideWorkTreeSpy = spyOn(gitLib, "isInsideGitWorkTree");
getUncommittedFilesSpy = spyOn(gitLib, "getUncommittedFiles");
confirmSpy = spyOn(clack, "confirm").mockResolvedValue(true);
Expand All @@ -35,6 +39,11 @@ afterEach(() => {
confirmSpy.mockRestore();
isCancelSpy.mockRestore();
logWarnSpy.mockRestore();
if (savedPlainOutput === undefined) {
delete process.env.SENTRY_PLAIN_OUTPUT;
} else {
process.env.SENTRY_PLAIN_OUTPUT = savedPlainOutput;
}
});

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