Skip to content

Commit 35e114f

Browse files
committed
fix: improve error quality and prevent secrets leaking in telemetry
Three fixes targeting the highest-impact issues from the 0.11.0 release: - fix(issue): include issue ID in 'not found' error and suggest short-ID format (CLI-N, 118 events / 41 users). Catches 404 from getIssue() in both the 'numeric' and 'explicit-org-numeric' resolveIssue() cases and re-throws a ContextError with the ID and a hint to try <project>-<id>. - fix(telemetry): redact --token value from telemetry tags (CLI-19, security). setFlagContext() now replaces values of sensitive flags (currently: token) with '[REDACTED]' before calling Sentry.setTag(). - fix(issue list): validate --cursor format early (CLI-7H/7P/7B). Cursor values that are plain integers are now rejected at parse time with a message explaining the expected format. The --limit issue is already addressed on main via auto-pagination (#274).
1 parent 3ec9dd9 commit 35e114f

File tree

4 files changed

+64
-6
lines changed

4 files changed

+64
-6
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ docs/dist
4040
docs/node_modules
4141
docs/.astro
4242

43+
# local planning notes (not for version control)
44+
.plans
45+
4346
# IntelliJ based IDEs
4447
.idea
4548

src/commands/issue/list.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ const VALID_SORT_VALUES: SortValue[] = ["date", "new", "freq", "user"];
8383
/** Usage hint for ContextError messages */
8484
const USAGE_HINT = "sentry issue list <org>/<project>";
8585

86+
/** Matches strings that are all digits — used to detect invalid cursor values */
87+
const ALL_DIGITS_RE = /^\d+$/;
88+
8689
/**
8790
* Maximum --limit value (user-facing ceiling for practical CLI response times).
8891
* Auto-pagination can theoretically fetch more, but 1000 keeps responses reasonable.
@@ -733,7 +736,21 @@ export const listCommand = buildCommand({
733736
json: LIST_JSON_FLAG,
734737
cursor: {
735738
kind: "parsed",
736-
parse: String,
739+
parse: (value: string) => {
740+
// "last" is the magic keyword to resume from the saved cursor
741+
if (value === "last") {
742+
return value;
743+
}
744+
// Sentry pagination cursors are opaque strings like "1735689600:0:0".
745+
// Plain integers are not valid cursors — catch this early so the user
746+
// gets a clear error rather than a cryptic 400 from the API.
747+
if (ALL_DIGITS_RE.test(value)) {
748+
throw new Error(
749+
`'${value}' is not a valid cursor. Cursors look like "1735689600:0:0". Use "last" to continue from the previous page.`
750+
);
751+
}
752+
return value;
753+
},
737754
// Issue-specific cursor brief: cursor only works in <org>/ mode
738755
brief:
739756
'Pagination cursor — only for <org>/ mode (use "last" to continue)',

src/commands/issue/utils.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
import { parseIssueArg } from "../../lib/arg-parsing.js";
1515
import { getProjectByAlias } from "../../lib/db/project-aliases.js";
1616
import { detectAllDsns } from "../../lib/dsn/index.js";
17-
import { ContextError } from "../../lib/errors.js";
17+
import { ApiError, ContextError } from "../../lib/errors.js";
1818
import { getProgressMessage } from "../../lib/formatters/seer.js";
1919
import { expandToFullShortId, isShortSuffix } from "../../lib/issue-id.js";
2020
import { poll } from "../../lib/polling.js";
@@ -255,8 +255,21 @@ export async function resolveIssue(
255255
switch (parsed.type) {
256256
case "numeric": {
257257
// Direct fetch by numeric ID - no org context
258-
const issue = await getIssue(parsed.id);
259-
return { org: undefined, issue };
258+
try {
259+
const issue = await getIssue(parsed.id);
260+
return { org: undefined, issue };
261+
} catch (err) {
262+
if (err instanceof ApiError && err.status === 404) {
263+
// Improve on the generic "Issue not found" message by including the ID
264+
// and suggesting the short-ID format, since users often confuse numeric
265+
// group IDs with short-ID suffixes.
266+
throw new ContextError(`Issue ${parsed.id}`, commandHint, [
267+
`No issue with numeric ID ${parsed.id} found — you may not have access, or it may have been deleted.`,
268+
`If this is a short ID suffix, try: sentry issue ${command} <project>-${parsed.id}`,
269+
]);
270+
}
271+
throw err;
272+
}
260273
}
261274

262275
case "explicit": {
@@ -268,8 +281,18 @@ export async function resolveIssue(
268281

269282
case "explicit-org-numeric": {
270283
// Org + numeric ID
271-
const issue = await getIssue(parsed.numericId);
272-
return { org: parsed.org, issue };
284+
try {
285+
const issue = await getIssue(parsed.numericId);
286+
return { org: parsed.org, issue };
287+
} catch (err) {
288+
if (err instanceof ApiError && err.status === 404) {
289+
throw new ContextError(`Issue ${parsed.numericId}`, commandHint, [
290+
`No issue with numeric ID ${parsed.numericId} found in org '${parsed.org}' — you may not have access, or it may have been deleted.`,
291+
`If this is a short ID suffix, try: sentry issue ${command} <project>-${parsed.numericId}`,
292+
]);
293+
}
294+
throw err;
295+
}
273296
}
274297

275298
case "explicit-org-suffix":

src/lib/telemetry.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,12 @@ export function setOrgProjectContext(orgs: string[], projects: string[]): void {
362362
}
363363
}
364364

365+
/**
366+
* Flag names whose values must never be sent to telemetry.
367+
* Values for these flags are replaced with "[REDACTED]" regardless of content.
368+
*/
369+
const SENSITIVE_FLAGS = new Set(["token"]);
370+
365371
/**
366372
* Set command flags as telemetry tags.
367373
*
@@ -373,6 +379,9 @@ export function setOrgProjectContext(orgs: string[], projects: string[]): void {
373379
* - String/number flags: only when defined and non-empty
374380
* - Array flags: only when non-empty
375381
*
382+
* Sensitive flags (e.g., `--token`) have their values replaced with
383+
* "[REDACTED]" to prevent secrets from reaching telemetry.
384+
*
376385
* Call this at the start of command func() to instrument flag usage.
377386
*
378387
* @param flags - The parsed flags object from Stricli
@@ -410,6 +419,12 @@ export function setFlagContext(flags: Record<string, unknown>): void {
410419
// Convert camelCase to kebab-case for consistency with CLI flag names
411420
const kebabKey = key.replace(/([A-Z])/g, "-$1").toLowerCase();
412421

422+
// Redact sensitive flag values (e.g., API tokens) — never send secrets to telemetry
423+
if (SENSITIVE_FLAGS.has(kebabKey)) {
424+
Sentry.setTag(`flag.${kebabKey}`, "[REDACTED]");
425+
continue;
426+
}
427+
413428
// Set the tag with flag. prefix
414429
// For booleans, just set "true"; for other types, convert to string
415430
const tagValue =

0 commit comments

Comments
 (0)