Skip to content
Merged
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
4 changes: 2 additions & 2 deletions docs/src/fragments/commands/init.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
# Interactive setup
sentry init

# Non-interactive with auto-yes
sentry init -y
# Non-interactive agent/CI setup
sentry init --yes --features errors,tracing,replay

# Dry run to preview changes
sentry init --dry-run
Expand Down
8 changes: 4 additions & 4 deletions plugins/sentry-cli/skills/sentry-cli/references/init.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ Initialize Sentry in your project (experimental)
Initialize Sentry in your project (experimental)

**Flags:**
- `-y, --yes - Non-interactive mode (accept defaults)`
- `-y, --yes - Accept non-interactive defaults (requires --features outside a TTY)`
- `-n, --dry-run - Show what would happen without making changes`
- `--features <value>... - Features to enable: errors,tracing,logs,replay,profiling,ai-monitoring,user-feedback`
- `--features <value>... - Features to enable: errors,tracing,logs,replay,metrics,profiling,sourcemaps,crons,ai-monitoring,user-feedback`
- `-t, --team <value> - Team slug to create the project under`
- `--tui - Use the Ink-based interactive UI (default on the Bun binary). Pass --no-tui to fall back to plain log output.`

Expand All @@ -28,8 +28,8 @@ Initialize Sentry in your project (experimental)
# Interactive setup
sentry init

# Non-interactive with auto-yes
sentry init -y
# Non-interactive agent/CI setup
sentry init --yes --features errors,tracing,replay

# Dry run to preview changes
sentry init --dry-run
Expand Down
111 changes: 104 additions & 7 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,45 @@ import {
const log = logger.withTag("init");

const FEATURE_DELIMITER = /[,+ ]+/;
const NON_INTERACTIVE_USAGE_HINT =
"sentry init --yes --features errors,tracing,replay [target] [directory]";

const USAGE_HINT = "sentry init <org>/<project> [directory]";

const FEATURE_ALIASES = {
errors: "errorMonitoring",
errorMonitoring: "errorMonitoring",
tracing: "performanceMonitoring",
performanceMonitoring: "performanceMonitoring",
logs: "logs",
replay: "sessionReplay",
sessionReplay: "sessionReplay",
metrics: "metrics",
profiling: "profiling",
sourcemaps: "sourceMaps",
sourceMaps: "sourceMaps",
crons: "crons",
"ai-monitoring": "aiMonitoring",
aiMonitoring: "aiMonitoring",
"user-feedback": "userFeedback",
userFeedback: "userFeedback",
} as const;

const SUPPORTED_FEATURE_NAMES = [
"errors",
"tracing",
"logs",
"replay",
"metrics",
"profiling",
"sourcemaps",
"crons",
"ai-monitoring",
"user-feedback",
] as const;

const SUPPORTED_FEATURE_TEXT = SUPPORTED_FEATURE_NAMES.join(", ");

type InitFlags = {
readonly yes: boolean;
readonly "dry-run": boolean;
Expand Down Expand Up @@ -109,6 +145,61 @@ function classifyArgs(
return { target: second, directory: first };
}

function parseFeatures(
features: readonly string[] | undefined
): string[] | undefined {
const requested = features
?.flatMap((feature) => feature.split(FEATURE_DELIMITER))
.map((feature) => feature.trim())
.filter(Boolean);

if (!requested || requested.length === 0) {
return;
}

return requested.map(normalizeFeature);
}

function normalizeFeature(feature: string): string {
const normalized = FEATURE_ALIASES[feature as keyof typeof FEATURE_ALIASES];
if (!normalized) {
throw new ValidationError(
`Unknown init feature "${feature}". Supported features: ${SUPPORTED_FEATURE_TEXT}`,
"features"
);
}
return normalized;
}

function isNonInteractiveContext(context: unknown): boolean {
const { stdin, stdout } = context as {
stdin?: { isTTY?: boolean };
stdout?: { isTTY?: boolean };
};
return stdin?.isTTY !== true || stdout?.isTTY !== true;
}

function validateNonInteractiveInit(
context: unknown,
flags: InitFlags,
features: readonly string[] | undefined
): void {
if (!isNonInteractiveContext(context)) {
return;
}

if (flags.yes && features && features.length > 0) {
return;
}

throw new ContextError("Yes flag and features", NON_INTERACTIVE_USAGE_HINT, [
"Agent/CI mode cannot ask interactive setup questions.",
"Pass --yes to accept non-interactive prompts.",
`Pass --features with one or more supported features: ${SUPPORTED_FEATURE_TEXT}.`,
"Run sentry init from an interactive terminal to use the wizard UI.",
]);
}

/**
* Resolve the parsed org/project target into explicit org and project values.
*
Expand Down Expand Up @@ -220,13 +311,17 @@ export const initCommand = buildCommand<
],
},
flags: {
yes: { ...YES_FLAG, brief: "Non-interactive mode (accept defaults)" },
yes: {
...YES_FLAG,
brief:
"Accept non-interactive defaults (requires --features outside a TTY)",
},
"dry-run": DRY_RUN_FLAG,
features: {
kind: "parsed",
parse: String,
brief:
"Features to enable: errors,tracing,logs,replay,profiling,ai-monitoring,user-feedback",
"Features to enable: errors,tracing,logs,replay,metrics,profiling,sourcemaps,crons,ai-monitoring,user-feedback",
variadic: true,
optional: true,
},
Expand Down Expand Up @@ -264,11 +359,13 @@ export const initCommand = buildCommand<
// 2. Resolve directory
const targetDir = dirArg ? path.resolve(this.cwd, dirArg) : this.cwd;

// 3. Parse features
const featuresList = flags.features
?.flatMap((f) => f.split(FEATURE_DELIMITER))
.map((f) => f.trim())
.filter(Boolean);
// 3. Parse and validate features before any network or wizard work.
const featuresList = parseFeatures(flags.features);

// Non-TTY callers (CI/agents) must provide every interactive choice
// needed to start setup. Fail before project lookup, org prefetch, or
// UI creation so the error is deterministic and actionable.
validateNonInteractiveInit(this, flags, featuresList);

// 4. Resolve target → org + project
// Validation of user-provided slugs happens inside resolveTarget.
Expand Down
24 changes: 12 additions & 12 deletions src/lib/init/clack-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,41 +37,41 @@ export function abortIfCancelled<T>(value: T): Exclude<T, symbol> {
const FEATURE_INFO: Record<string, { label: string; hint: string }> = {
errorMonitoring: {
label: "Error Monitoring",
hint: "Error and crash reporting",
hint: "Group exceptions into issues with context",
},
performanceMonitoring: {
label: "Performance Monitoring (Tracing)",
hint: "Transaction and span tracing",
label: "Tracing",
hint: "See request paths, spans, and bottlenecks",
},
sessionReplay: {
label: "Session Replay",
hint: "Visual replay of user sessions",
hint: "Replay sessions linked to errors",
},
profiling: {
label: "Profiling",
hint: "Code-level performance insights",
hint: "Find CPU-heavy functions in production",
},
logs: { label: "Logging", hint: "Structured log ingestion" },
metrics: { label: "Metrics", hint: "Track business metrics" },
logs: { label: "Logging", hint: "Search logs beside errors and traces" },
metrics: { label: "Metrics", hint: "Track custom measurements over time" },
sourceMaps: {
label: "Source Maps",
hint: "See original source code in production errors",
hint: "Turn minified stacks into your source",
},
crons: {
label: "Crons",
hint: "Monitor scheduled and recurring jobs",
hint: "Alert on failed or missed scheduled jobs",
},
aiMonitoring: {
label: "AI Monitoring",
hint: "Track AI model calls, latency, and failures",
hint: "Track AI calls, latency, cost, and failures",
},
userFeedback: {
label: "User Feedback",
hint: "Collect in-app user feedback and reports",
hint: "Collect user reports with issue context",
},
reactFeatures: {
label: "React Features",
hint: "Redux, component tracking, source maps, and integrations",
hint: "Add React-specific context and integrations",
},
};

Expand Down
23 changes: 23 additions & 0 deletions src/lib/init/feedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export type InitFeedbackOutcome = "success" | "cancelled" | "failed";

const FEEDBACK_COMMANDS: Record<InitFeedbackOutcome, string> = {
success: '$ sentry cli feedback "sentry init worked well"',
cancelled: '$ sentry cli feedback "sentry init was cancelled"',
failed: '$ sentry cli feedback "sentry init failed"',
};

const FEEDBACK_COPY: Record<InitFeedbackOutcome, string[]> = {
success: [
"Nice, setup made it through.",
"Tell us what felt great or rough:",
],
cancelled: [
"Sad to see setup stop. Was something going sideways?",
"Tell us so we can fix it:",
],
failed: ["Setup hit a wall.", "Tell us what happened so we can fix it:"],
};

export function formatFeedbackHint(outcome: InitFeedbackOutcome): string {
return [...FEEDBACK_COPY[outcome], FEEDBACK_COMMANDS[outcome]].join("\n");
}
5 changes: 2 additions & 3 deletions src/lib/init/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,9 @@ export function formatResult(result: WorkflowRunResult, ui: WizardUI): void {
}

ui.log.info("Please review the changes above before committing.");
ui.log.info(
"You're one of the first to try the new setup wizard! Run `sentry cli feedback` to let us know how it went."
);

ui.outro("Sentry SDK installed successfully!");
ui.feedback("success");
}

export function formatError(result: WorkflowRunResult, ui: WizardUI): void {
Expand Down Expand Up @@ -125,4 +123,5 @@ export function formatError(result: WorkflowRunResult, ui: WizardUI): void {
}

ui.cancel("Setup failed");
ui.feedback("failed");
}
17 changes: 11 additions & 6 deletions src/lib/init/interactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ import type {
} from "./types.js";
import type { WizardUI } from "./ui/types.js";

function prependRequiredFeature(
features: string[],
hasRequired: boolean
): string[] {
if (!(hasRequired && !features.includes(REQUIRED_FEATURE))) {
return features;
}
return [REQUIRED_FEATURE, ...features];
}

export async function handleInteractive(
payload: InteractivePayload,
options: InteractiveContext,
Expand Down Expand Up @@ -133,16 +143,11 @@ async function handleMultiSelect(
...(hint ? { hint } : {}),
};
}),
initialValues: optional.filter((f) => f === "performanceMonitoring"),
required: false,
});

const chosen = abortIfCancelled(selected);
if (hasRequired && !chosen.includes(REQUIRED_FEATURE)) {
chosen.unshift(REQUIRED_FEATURE);
}

return { features: chosen };
return { features: prependRequiredFeature(chosen, hasRequired) };
}

async function handleConfirm(
Expand Down
2 changes: 2 additions & 0 deletions src/lib/init/preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,15 @@ async function withPreflightHandling(
} catch (error) {
if (error instanceof WizardCancelledError) {
ui.cancel("Setup cancelled.");
ui.feedback("cancelled");
process.exitCode = 0;
return null;
}

const message = error instanceof Error ? error.message : String(error);
ui.log.error(message);
ui.cancel("Setup failed.");
ui.feedback("failed");
throw error instanceof WizardError ? error : new WizardError(message);
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/lib/init/readiness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export async function checkReadiness(ui: WizardUI): Promise<void> {
ui.log.info("Run `sentry auth login` to authenticate.");
ui.log.info("Check your network connection and try again.");
ui.cancel("Setup failed");
ui.feedback("failed");
throw new WizardError("Pre-flight checks failed");
}

Expand All @@ -43,11 +44,12 @@ export async function checkReadiness(ui: WizardUI): Promise<void> {
ui.log.error("No authentication token found.");
ui.log.info("Run `sentry auth login` to authenticate, then try again.");
ui.cancel("Setup failed");
ui.feedback("failed");
throw new WizardError("Not authenticated");
}

if (apiOk) {
spin.stop("Prerequisites OK");
spin.stop("");
} else {
spin.stop("Warning", 2);
ui.log.warn(
Expand Down
10 changes: 8 additions & 2 deletions src/lib/init/ui/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
*/

import { LoggingUI } from "./logging-ui.js";
import type { WizardUI } from "./types.js";
import type { WelcomeOptions, WizardUI } from "./types.js";

/**
* Inputs that affect UI selection. Mirrors the relevant subset of
Expand All @@ -43,6 +43,12 @@ import type { WizardUI } from "./types.js";
export type UIFactoryOptions = {
/** True when `--yes` (or `--dry-run`, which implies non-interactive) is set. */
yes: boolean;
/**
* Optional first Ink prompt to seed before `ink.render()` is called.
* This keeps the first painted frame from flashing the normal wizard
* shell before the welcome screen is ready.
*/
initialWelcome?: WelcomeOptions;
/**
* True when the user explicitly opted out of the new TUI via
* `--no-tui`. Forces `LoggingUI`.
Expand Down Expand Up @@ -118,7 +124,7 @@ export async function getUIAsync(opts: UIFactoryOptions): Promise<WizardUI> {
}
try {
const { createInkUI } = await import("./ink-ui.js");
return await createInkUI();
return await createInkUI({ initialWelcome: opts.initialWelcome });
} catch {
// Fall through to LoggingUI so a missing/broken Ink install
// doesn't take down the wizard. This branch should be
Expand Down
Loading
Loading