Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
5 changes: 1 addition & 4 deletions src/lib/init/feedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ const FEEDBACK_COMMANDS: Record<InitFeedbackOutcome, string> = {
};

const FEEDBACK_COPY: Record<InitFeedbackOutcome, string[]> = {
success: [
"Nice, setup made it through.",
"Tell us what felt great or rough:",
],
success: ["Tell us what felt great or rough:"],
cancelled: [
"Sad to see setup stop. Was something going sideways?",
"Tell us so we can fix it:",
Expand Down
24 changes: 21 additions & 3 deletions src/lib/init/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
*/

import { terminalLink } from "../formatters/colors.js";
import { featureLabel } from "./clack-utils.js";
import { featureLabel, sortFeatures } from "./clack-utils.js";
import {
EXIT_DEPENDENCY_INSTALL_FAILED,
EXIT_PLATFORM_NOT_DETECTED,
Expand All @@ -41,11 +41,11 @@
if (output.projectDir) {
fields.push({ label: "Directory", value: output.projectDir });
}
if (output.features?.length) {
if (output.features?.length && !output.featureBlurbs?.length) {
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
fields.push({
label: "Features",
value: output.features.map(featureLabel).join(", "),
});

Check warning on line 48 in src/lib/init/formatters.ts

View check run for this annotation

@sentry/warden / warden: find-bugs

Features silently disappear when server returns blurbs with empty strings

The plain "Features" field is suppressed whenever `output.featureBlurbs` is non-empty (line 44), but the blurbs table is only rendered after filtering out null/empty entries. If the server returns any `featureBlurbs` entries where every `blurb` is an empty string, the Features row is suppressed yet the blurbs table is also empty — no feature information appears in the summary at all.
}
if (output.commands?.length) {
fields.push({
Expand All @@ -62,13 +62,31 @@

const changedFiles = output.changedFiles ?? [];

if (fields.length === 0 && changedFiles.length === 0) {
// output.features is the canonical ordered list of selected feature IDs.
// Pair blurbs positionally so labels are always correct regardless of what
// the agent echoes back in the feature field.
const blurbsInOrder = output.featureBlurbs ?? [];
const canonicalFeatures = output.features ?? [];
const featureBlurbs = sortFeatures(canonicalFeatures)
.map((feature) => {
const pos = canonicalFeatures.indexOf(feature);
const blurb = blurbsInOrder[pos]?.blurb;
return blurb ? { label: featureLabel(feature), blurb } : null;
})
.filter((b): b is { label: string; blurb: string } => b !== null);
Comment thread
sentry-warden[bot] marked this conversation as resolved.
Outdated

if (
fields.length === 0 &&
changedFiles.length === 0 &&
featureBlurbs.length === 0
) {
return null;
}

return {
fields,
...(changedFiles.length > 0 ? { changedFiles } : {}),
...(featureBlurbs.length > 0 ? { featureBlurbs } : {}),
};
}

Expand Down
1 change: 1 addition & 0 deletions src/lib/init/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ export type WizardOutput = {
docsUrl?: string;
sentryProjectUrl?: string;
message?: string;
featureBlurbs?: Array<{ feature: string; blurb: string }>;
};

// Interactive payloads
Expand Down
22 changes: 22 additions & 0 deletions src/lib/init/ui/ink-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1176,6 +1176,28 @@ function SummaryPanel({
{summary.changedFiles !== undefined && summary.changedFiles.length > 0 ? (
<ChangedFilesTree files={summary.changedFiles} />
) : null}
{summary.featureBlurbs !== undefined &&
summary.featureBlurbs.length > 0 ? (
<Box flexDirection="column" flexShrink={0} marginTop={1}>
<Text bold color={MUTED}>
Here&apos;s what we set up
</Text>
{summary.featureBlurbs.map(({ label, blurb }) => (
<Box flexDirection="row" flexShrink={0} key={label}>
<Box flexShrink={0} width={22}>
<Text bold color={PRIMARY}>
{label}
</Text>
</Box>
<Box flexShrink={1}>
<Text color={MUTED} wrap="wrap">
{blurb}
</Text>
</Box>
</Box>
))}
</Box>
) : null}
Comment thread
cursor[bot] marked this conversation as resolved.
</Box>
);
}
Expand Down
15 changes: 15 additions & 0 deletions src/lib/init/ui/ink-report.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import chalk from "chalk";
import { renderTextTable } from "../../formatters/text-table.js";
import { buildFileTree, flattenTree } from "./file-tree.js";
import type { WizardSummary } from "./types.js";

Expand Down Expand Up @@ -68,6 +69,20 @@ export function formatSuccessReport(
lines.push(formatTreeRowChalk(row));
}
}
if (summary?.featureBlurbs && summary.featureBlurbs.length > 0) {
lines.push("");
lines.push(` ${chalk.hex(REPORT_MUTED).bold("Here's what we set up")}`);
const tableRows = summary.featureBlurbs.map(({ label, blurb }) => [
chalk.bold(label),
chalk.hex(REPORT_MUTED)(blurb),
Comment thread
sentry-warden[bot] marked this conversation as resolved.
]);
const table = renderTextTable(["", ""], tableRows, {
shrinkable: [false, true],
});
for (const line of table.trimEnd().split("\n")) {
lines.push(` ${line}`);
}
}
appendFeedbackHint(lines, feedbackHint);
return lines.join("\n");
}
Expand Down
21 changes: 20 additions & 1 deletion src/lib/init/ui/logging-ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
renderInlineMarkdown,
renderMarkdown,
} from "../../formatters/markdown.js";
import { renderTextTable } from "../../formatters/text-table.js";
import { formatFeedbackHint, type InitFeedbackOutcome } from "../feedback.js";
import { buildFileTree, flattenTree } from "./file-tree.js";
import type {
Expand Down Expand Up @@ -94,7 +95,11 @@ export class LoggingUI implements WizardUI {
}

summary(summary: WizardSummary): void {
if (summary.fields.length === 0 && !summary.changedFiles?.length) {
if (
summary.fields.length === 0 &&
!summary.changedFiles?.length &&
!summary.featureBlurbs?.length
) {
return;
}
// Compact two-column key/value listing — one line per field. The
Expand All @@ -119,6 +124,20 @@ export class LoggingUI implements WizardUI {
this.writeLine(this.stdout, ` ${formatTreeRowPlain(row)}`);
}
}
if (summary.featureBlurbs && summary.featureBlurbs.length > 0) {
this.writeLine(this.stdout, "");
this.writeLine(this.stdout, " Here's what we set up");
const tableRows = summary.featureBlurbs.map(({ label, blurb }) => [
label,
blurb,
]);
const table = renderTextTable(["", ""], tableRows, {
shrinkable: [false, true],
});
for (const line of table.trimEnd().split("\n")) {
this.writeLine(this.stdout, ` ${line}`);
}
}
}

outro(message: string): void {
Expand Down
2 changes: 2 additions & 0 deletions src/lib/init/ui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ export type WizardSummary = {
fields: { label: string; value: string }[];
/** Optional list of files the wizard added/edited/removed. */
changedFiles?: { action: string; path: string }[];
/** AI-generated per-feature blurbs personalised to the analysed project. */
featureBlurbs?: { label: string; blurb: string }[];
};

/**
Expand Down
1 change: 0 additions & 1 deletion test/lib/init/feedback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ describe("formatFeedbackHint", () => {
test("maps init outcomes to copy-paste feedback commands", () => {
expect(formatFeedbackHint("success")).toBe(
[
"Nice, setup made it through.",
"Tell us what felt great or rough:",
'$ sentry cli feedback "sentry init worked well"',
].join("\n")
Expand Down
114 changes: 114 additions & 0 deletions test/lib/init/formatters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,120 @@ describe("formatResult", () => {
});
});

describe("formatResult with featureBlurbs", () => {
test("populates featureBlurbs from output.featureBlurbs paired positionally with output.features", () => {
const { ui, calls } = createMockUI();
formatResult(
{
status: "success",
result: {
platform: "Next.js",
projectDir: "/app",
features: ["errorMonitoring", "performanceMonitoring"],
featureBlurbs: [
{ feature: "errorMonitoring", blurb: "Captures exceptions." },
{ feature: "performanceMonitoring", blurb: "Traces requests." },
],
},
},
ui
);

const summary = summaryCall(calls);
expect(summary?.featureBlurbs).toEqual([
{ label: "Error Monitoring", blurb: "Captures exceptions." },
{ label: "Tracing", blurb: "Traces requests." },
]);
});

test("suppresses the Features row when featureBlurbs are present", () => {
const { ui, calls } = createMockUI();
formatResult(
{
status: "success",
result: {
platform: "Next.js",
features: ["errorMonitoring"],
featureBlurbs: [
{ feature: "errorMonitoring", blurb: "Captures exceptions." },
],
},
},
ui
);

const summary = summaryCall(calls);
expect(summary?.fields.some((f) => f.label === "Features")).toBe(false);
});

test("shows the Features row when featureBlurbs are absent", () => {
const { ui, calls } = createMockUI();
formatResult(
{
status: "success",
result: {
platform: "Next.js",
features: ["errorMonitoring", "sessionReplay"],
},
},
ui
);

const summary = summaryCall(calls);
expect(summary?.fields.some((f) => f.label === "Features")).toBe(true);
});

test("uses output.features for labels regardless of what the agent echoed in feature field", () => {
const { ui, calls } = createMockUI();
formatResult(
{
status: "success",
result: {
platform: "Next.js",
features: ["errorMonitoring", "sessionReplay"],
// Agent echoed back wrong IDs
featureBlurbs: [
{ feature: "error_monitoring", blurb: "Blurb A." },
{ feature: "session-replay", blurb: "Blurb B." },
],
},
},
ui
);

const summary = summaryCall(calls);
// Labels come from output.features positionally, not blurb.feature
expect(summary?.featureBlurbs?.[0]?.label).toBe("Error Monitoring");
expect(summary?.featureBlurbs?.[1]?.label).toBe("Session Replay");
});

test("sorts featureBlurbs by canonical display order", () => {
const { ui, calls } = createMockUI();
formatResult(
{
status: "success",
result: {
platform: "Next.js",
// Server returned performanceMonitoring before errorMonitoring
features: ["performanceMonitoring", "errorMonitoring"],
featureBlurbs: [
{ feature: "performanceMonitoring", blurb: "Traces." },
{ feature: "errorMonitoring", blurb: "Captures." },
],
},
},
ui
);

const summary = summaryCall(calls);
// errorMonitoring comes before performanceMonitoring in FEATURE_DISPLAY_ORDER
expect(summary?.featureBlurbs?.map((b) => b.label)).toEqual([
"Error Monitoring",
"Tracing",
]);
});
});

describe("formatError", () => {
test("logs the error message", () => {
const { ui, calls } = createMockUI();
Expand Down
31 changes: 31 additions & 0 deletions test/lib/init/ui/ink-report.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,37 @@ describe("formatSuccessReport with summary fields", () => {
});
});

describe("formatSuccessReport with featureBlurbs", () => {
test("renders Here's what we set up heading and blurb content", () => {
const output = stripAnsi(
formatSuccessReport("Done!", {
fields: [],
featureBlurbs: [
{ label: "Error Monitoring", blurb: "Captures exceptions." },
{ label: "Tracing", blurb: "Traces requests end-to-end." },
],
})
);
expect(output).toContain("Here's what we set up");
expect(output).toContain("Error Monitoring");
expect(output).toContain("Captures exceptions.");
expect(output).toContain("Tracing");
expect(output).toContain("Traces requests end-to-end.");
});

test("no blurbs section when featureBlurbs is absent", () => {
const output = stripAnsi(formatSuccessReport("Done!", { fields: [] }));
expect(output).not.toContain("Here's what we set up");
});

test("no blurbs section when featureBlurbs is empty", () => {
const output = stripAnsi(
formatSuccessReport("Done!", { fields: [], featureBlurbs: [] })
);
expect(output).not.toContain("Here's what we set up");
});
});

describe("formatSuccessReport with changedFiles", () => {
test("shows Changed files heading and file paths", () => {
const output = stripAnsi(
Expand Down
23 changes: 23 additions & 0 deletions test/lib/init/ui/logging-ui.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,4 +307,27 @@ describe("LoggingUI summary", () => {
expect(lines.some((l) => l.includes("−") && l.includes("b.ts"))).toBe(true);
expect(lines.some((l) => l.includes("~") && l.includes("c.ts"))).toBe(true);
});

test("featureBlurbs renders Here's what we set up table with label and blurb", () => {
const { ui, stdout } = createUI();
ui.summary({
fields: [],
featureBlurbs: [
{ label: "Error Monitoring", blurb: "Captures exceptions." },
{ label: "Tracing", blurb: "Traces requests." },
],
});
const out = stdout();
expect(out).toContain("Here's what we set up");
expect(out).toContain("Error Monitoring");
expect(out).toContain("Captures exceptions.");
expect(out).toContain("Tracing");
expect(out).toContain("Traces requests.");
});

test("no featureBlurbs section when featureBlurbs is absent", () => {
const { ui, stdout } = createUI();
ui.summary({ fields: [{ label: "Platform", value: "Next.js" }] });
expect(stdout()).not.toContain("Here's what we set up");
});
});
Loading