Skip to content

Commit b17c30b

Browse files
committed
fix: address PR review feedback
- Use getControlSiloUrl() for /customers/ endpoints (control silo, not region-scoped) - Replace stderr.write() with consola logger in seer-trial.ts - Move instanceof SeerError check into isTrialEligible() (accepts unknown) - Fix timezone bug: use setUTCHours in getTrialStatus() - Soften unconditional trial hint wording in SeerError.format() - Update seer-trial tests for new API (logger mocks, 2-arg signature)
1 parent e4c3bb5 commit b17c30b

File tree

6 files changed

+128
-112
lines changed

6 files changed

+128
-112
lines changed

src/bin.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,7 @@ import { isatty } from "node:tty";
22
import { run } from "@stricli/core";
33
import { app } from "./app.js";
44
import { buildContext } from "./context.js";
5-
import {
6-
AuthError,
7-
formatError,
8-
getExitCode,
9-
SeerError,
10-
} from "./lib/errors.js";
5+
import { AuthError, formatError, getExitCode } from "./lib/errors.js";
116
import { error } from "./lib/formatters/colors.js";
127
import { runInteractiveLogin } from "./lib/interactive-login.js";
138
import { getEnvLogLevel, setLogLevel } from "./lib/logger.js";
@@ -58,13 +53,13 @@ async function executeWithSeerTrialPrompt(args: string[]): Promise<void> {
5853
try {
5954
await runCommand(args);
6055
} catch (err) {
61-
if (err instanceof SeerError && isTrialEligible(err)) {
62-
// isTrialEligible ensures orgSlug is defined
56+
// isTrialEligible handles instanceof SeerError check + reason + orgSlug + TTY
57+
if (isTrialEligible(err)) {
58+
// isTrialEligible narrows err to SeerError with defined orgSlug
6359
const started = await promptAndStartTrial(
6460
// biome-ignore lint/style/noNonNullAssertion: isTrialEligible guarantees orgSlug is defined
6561
err.orgSlug!,
66-
err.reason,
67-
process.stderr
62+
err.reason
6863
);
6964

7065
if (started) {

src/lib/api-client.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1707,9 +1707,9 @@ export async function triggerSolutionPlanning(
17071707
export async function getProductTrials(
17081708
orgSlug: string
17091709
): Promise<ProductTrial[]> {
1710-
const regionUrl = await resolveOrgRegion(orgSlug);
1710+
// /customers/ is a control silo endpoint (billing), not region-scoped
17111711
const { data } = await apiRequestToRegion<CustomerTrialInfo>(
1712-
regionUrl,
1712+
getControlSiloUrl(),
17131713
`/customers/${orgSlug}/`,
17141714
{ schema: CustomerTrialInfoSchema }
17151715
);
@@ -1730,14 +1730,18 @@ export async function startProductTrial(
17301730
orgSlug: string,
17311731
category: string
17321732
): Promise<void> {
1733-
const regionUrl = await resolveOrgRegion(orgSlug);
1734-
await apiRequestToRegion(regionUrl, `/customers/${orgSlug}/product-trial/`, {
1735-
method: "PUT",
1736-
body: {
1737-
referrer: "sentry-cli",
1738-
productTrial: { category, reasonCode: 0 },
1739-
},
1740-
});
1733+
// /customers/ is a control silo endpoint (billing), not region-scoped
1734+
await apiRequestToRegion(
1735+
getControlSiloUrl(),
1736+
`/customers/${orgSlug}/product-trial/`,
1737+
{
1738+
method: "PUT",
1739+
body: {
1740+
referrer: "sentry-cli",
1741+
productTrial: { category, reasonCode: 0 },
1742+
},
1743+
}
1744+
);
17411745
}
17421746

17431747
// User functions

src/lib/errors.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,11 @@ export class SeerError extends CliError {
371371
}
372372

373373
override format(): string {
374-
const trialHint = "\n\nOr start a free trial:\n sentry trial start seer";
374+
// Soften trial hint — we can't check availability synchronously,
375+
// so use "check" language rather than "start" to avoid misleading
376+
// users whose trial is already expired
377+
const trialHint =
378+
"\n\nYou may be eligible for a free trial:\n sentry trial list";
375379

376380
// When org slug is known, provide direct URLs to settings
377381
if (this.orgSlug) {

src/lib/seer-trial.ts

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
import { isatty } from "node:tty";
1414

1515
import { getProductTrials, startProductTrial } from "./api-client.js";
16-
import type { SeerError, SeerErrorReason } from "./errors.js";
17-
import { success } from "./formatters/colors.js";
16+
import { SeerError, type SeerErrorReason } from "./errors.js";
1817
import { logger } from "./logger.js";
18+
import { buildBillingUrl } from "./sentry-urls.js";
1919
import { findAvailableTrial } from "./trials.js";
2020

2121
/** Seer error reasons eligible for trial prompt */
@@ -31,17 +31,21 @@ const REASON_CONTEXT: Record<string, string> = {
3131
};
3232

3333
/**
34-
* Check whether a SeerError is eligible for a trial prompt.
34+
* Check whether an error is a trial-eligible SeerError.
35+
*
36+
* Performs the `instanceof SeerError` check internally so callers
37+
* can pass any caught error without narrowing first.
3538
*
3639
* Only `no_budget` and `not_enabled` are eligible — `ai_disabled` is
3740
* an explicit admin decision that a trial wouldn't override.
3841
* Requires orgSlug (needed for API calls) and interactive terminal.
3942
*
40-
* @param error - The SeerError to check
41-
* @returns true if the error is eligible for a trial prompt
43+
* @param error - Any caught error
44+
* @returns true if the error is a SeerError eligible for a trial prompt
4245
*/
43-
export function isTrialEligible(error: SeerError): boolean {
46+
export function isTrialEligible(error: unknown): error is SeerError {
4447
return (
48+
error instanceof SeerError &&
4549
TRIAL_ELIGIBLE_REASONS.has(error.reason) &&
4650
error.orgSlug !== undefined &&
4751
isatty(0)
@@ -56,16 +60,18 @@ export function isTrialEligible(error: SeerError): boolean {
5660
* 2. Show context message + prompt user for confirmation
5761
* 3. Start the trial via API
5862
*
63+
* Uses consola logger for all output (not raw stderr writes).
64+
*
5965
* @param orgSlug - Organization slug
6066
* @param reason - The SeerError reason (for context message)
61-
* @param stderr - Stderr stream for messages
6267
* @returns true if trial was started successfully, false otherwise
6368
*/
6469
export async function promptAndStartTrial(
6570
orgSlug: string,
66-
reason: SeerErrorReason,
67-
stderr: NodeJS.WriteStream
71+
reason: SeerErrorReason
6872
): Promise<boolean> {
73+
const log = logger.withTag("seer");
74+
6975
// 1. Check trial availability (graceful failure → return false)
7076
let trial: ReturnType<typeof findAvailableTrial> extends infer T ? T : never;
7177
try {
@@ -77,18 +83,22 @@ export async function promptAndStartTrial(
7783
}
7884

7985
if (!trial) {
80-
// No trial available — fall through to normal error
86+
// No trial available (expired or already used)
87+
log.info(
88+
"No Seer trial available. If you've already used your trial, " +
89+
"consider upgrading your plan to continue using Seer."
90+
);
91+
log.info(` ${buildBillingUrl(orgSlug, "seer")}`);
8192
return false;
8293
}
8394

8495
// 2. Show context and prompt
85-
const context = REASON_CONTEXT[reason] ?? "";
96+
const context = REASON_CONTEXT[reason];
8697
if (context) {
87-
stderr.write(`${context}\n`);
98+
log.info(context);
8899
}
89100

90101
const daysText = trial.lengthDays ? `${trial.lengthDays}-day ` : "";
91-
const log = logger.withTag("seer");
92102
const confirmed = await log.prompt(
93103
`A free ${daysText}Seer trial is available. Start trial?`,
94104
{ type: "confirm", initial: true }
@@ -101,14 +111,15 @@ export async function promptAndStartTrial(
101111

102112
// 3. Start trial using the category from the available trial
103113
try {
104-
stderr.write("\nStarting Seer trial...\n");
114+
log.info("Starting Seer trial...");
105115
await startProductTrial(orgSlug, trial.category);
106-
stderr.write(`${success("✓")} Seer trial activated!\n`);
116+
log.success("Seer trial activated!");
107117
return true;
108118
} catch {
109-
stderr.write(
110-
"Failed to start trial. Please try again or visit your Sentry settings.\n"
119+
log.warn(
120+
"Failed to start trial. Please try again or visit your Sentry settings:"
111121
);
122+
log.warn(` ${buildBillingUrl(orgSlug, "seer")}`);
112123
return false;
113124
}
114125
}

src/lib/trials.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,8 @@ export function getTrialStatus(trial: ProductTrial): TrialStatus {
143143
if (trial.endDate) {
144144
const end = new Date(trial.endDate);
145145
const now = new Date();
146-
// Compare date-only (ignore time) — trial ends at end of endDate
147-
end.setHours(23, 59, 59, 999);
146+
// Compare date-only (ignore time) — trial ends at end of endDate UTC
147+
end.setUTCHours(23, 59, 59, 999);
148148
if (now > end) {
149149
return "expired";
150150
}

0 commit comments

Comments
 (0)