Skip to content

Commit 1e4ebe0

Browse files
committed
fix: address all bot review comments from PR #262
- Fix malformed 'project:' filter: empty projectSlug now produces empty string (falsy) so .filter(Boolean) correctly removes it for org-wide listing - Fix isMultiProject=false in org-all issue listing: set to true so the ALIAS column shows which project each issue belongs to - Fix missing hasMore check on empty page in issue list org-all path: show 'try the next page' hint instead of definitive 'no issues' when hasMore=true - Remove redundant footer tip from handleOrgAll in repo/list and team/list: the tip told users to do what they already did - Deduplicate buildContextKey/resolveCursor: extract buildOrgContextKey and resolveOrgCursor into src/lib/db/pagination.ts and update all callers
1 parent de709bf commit 1e4ebe0

File tree

5 files changed

+85
-103
lines changed

5 files changed

+85
-103
lines changed

src/commands/issue/list.ts

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { parseOrgProjectArg } from "../../lib/arg-parsing.js";
1717
import { buildCommand, numberParser } from "../../lib/command.js";
1818
import {
1919
clearPaginationCursor,
20-
getPaginationCursor,
20+
resolveOrgCursor,
2121
setPaginationCursor,
2222
} from "../../lib/db/pagination.js";
2323
import {
@@ -468,22 +468,9 @@ export const listCommand = buildCommand({
468468
// Handle org-all mode with cursor pagination (different code path)
469469
if (parsed.type === "org-all") {
470470
const org = parsed.org;
471+
// Issue cursors encode sort+query so different searches don't share pages.
471472
const contextKey = `host:${getApiBaseUrl()}|type:org:${org}|sort:${flags.sort}${flags.query ? `|q:${flags.query}` : ""}`;
472-
let cursor: string | undefined;
473-
if (flags.cursor) {
474-
if (flags.cursor === "last") {
475-
const cached = getPaginationCursor(PAGINATION_KEY, contextKey);
476-
if (!cached) {
477-
throw new ContextError(
478-
"Pagination cursor",
479-
"No saved cursor for this query. Run without --cursor first."
480-
);
481-
}
482-
cursor = cached;
483-
} else {
484-
cursor = flags.cursor;
485-
}
486-
}
473+
const cursor = resolveOrgCursor(flags.cursor, PAGINATION_KEY, contextKey);
487474

488475
setContext([org], []);
489476

@@ -518,17 +505,24 @@ export const listCommand = buildCommand({
518505
}
519506

520507
if (response.data.length === 0) {
521-
stdout.write(`No issues found in organization '${org}'.\n`);
508+
if (hasMore) {
509+
const hint = `sentry issue list ${org}/ -c last`;
510+
stdout.write(`No issues on this page. Try the next page: ${hint}\n`);
511+
} else {
512+
stdout.write(`No issues found in organization '${org}'.\n`);
513+
}
522514
return;
523515
}
524516

525517
writeListHeader(stdout, `Issues in ${org}`, false);
526518
const termWidth = process.stdout.columns || 80;
519+
// isMultiProject=true so the ALIAS column shows which project each issue
520+
// belongs to — essential when viewing issues across an entire org.
527521
const issuesWithOpts = response.data.map((issue) => ({
528522
issue,
529523
formatOptions: {
530524
projectSlug: issue.project?.slug ?? "",
531-
isMultiProject: false,
525+
isMultiProject: true,
532526
},
533527
}));
534528
writeIssueRows(stdout, issuesWithOpts, termWidth);

src/commands/repo/list.ts

Lines changed: 9 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ import {
2020
import { parseOrgProjectArg } from "../../lib/arg-parsing.js";
2121
import { buildCommand, numberParser } from "../../lib/command.js";
2222
import {
23+
buildOrgContextKey,
2324
clearPaginationCursor,
24-
getPaginationCursor,
25+
resolveOrgCursor,
2526
setPaginationCursor,
2627
} from "../../lib/db/pagination.js";
27-
import { AuthError, ContextError, ValidationError } from "../../lib/errors.js";
28+
import { AuthError, ValidationError } from "../../lib/errors.js";
2829
import { writeFooter, writeJson } from "../../lib/formatters/index.js";
2930
import { resolveOrgsForListing } from "../../lib/resolve-target.js";
30-
import { getApiBaseUrl } from "../../lib/sentry-client.js";
3131
import type { SentryRepository, Writer } from "../../types/index.js";
3232

3333
/** Command key for pagination cursor storage */
@@ -143,38 +143,6 @@ async function fetchAllOrgRepositories(): Promise<RepositoryWithOrg[]> {
143143
return results;
144144
}
145145

146-
/**
147-
* Build a context key for pagination cursor validation.
148-
* Captures the org so cursors from different orgs are never mixed.
149-
*/
150-
function buildContextKey(org: string): string {
151-
return `host:${getApiBaseUrl()}|type:org:${org}`;
152-
}
153-
154-
/**
155-
* Resolve the cursor value from --cursor flag.
156-
* Handles the magic "last" value by looking up the cached cursor.
157-
*/
158-
function resolveCursor(
159-
cursorFlag: string | undefined,
160-
contextKey: string
161-
): string | undefined {
162-
if (!cursorFlag) {
163-
return;
164-
}
165-
if (cursorFlag === "last") {
166-
const cached = getPaginationCursor(PAGINATION_KEY, contextKey);
167-
if (!cached) {
168-
throw new ContextError(
169-
"Pagination cursor",
170-
"No saved cursor for this query. Run without --cursor first."
171-
);
172-
}
173-
return cached;
174-
}
175-
return cursorFlag;
176-
}
177-
178146
/** Build the CLI hint for fetching the next page. */
179147
function nextPageHint(org: string): string {
180148
return `sentry repo list ${org}/ -c last`;
@@ -237,11 +205,6 @@ async function handleOrgAll(options: OrgAllOptions): Promise<void> {
237205
} else {
238206
stdout.write(`\nShowing ${repos.length} repositories\n`);
239207
}
240-
241-
writeFooter(
242-
stdout,
243-
"Tip: Use 'sentry repo list <org>/' for paginated results"
244-
);
245208
}
246209

247210
/**
@@ -435,8 +398,12 @@ export const listCommand = buildCommand({
435398
break;
436399

437400
case "org-all": {
438-
const contextKey = buildContextKey(parsed.org);
439-
const cursor = resolveCursor(flags.cursor, contextKey);
401+
const contextKey = buildOrgContextKey(parsed.org);
402+
const cursor = resolveOrgCursor(
403+
flags.cursor,
404+
PAGINATION_KEY,
405+
contextKey
406+
);
440407
await handleOrgAll({
441408
stdout,
442409
org: parsed.org,

src/commands/team/list.ts

Lines changed: 9 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ import {
2020
import { parseOrgProjectArg } from "../../lib/arg-parsing.js";
2121
import { buildCommand, numberParser } from "../../lib/command.js";
2222
import {
23+
buildOrgContextKey,
2324
clearPaginationCursor,
24-
getPaginationCursor,
25+
resolveOrgCursor,
2526
setPaginationCursor,
2627
} from "../../lib/db/pagination.js";
27-
import { AuthError, ContextError, ValidationError } from "../../lib/errors.js";
28+
import { AuthError, ValidationError } from "../../lib/errors.js";
2829
import { writeFooter, writeJson } from "../../lib/formatters/index.js";
2930
import { resolveOrgsForListing } from "../../lib/resolve-target.js";
30-
import { getApiBaseUrl } from "../../lib/sentry-client.js";
3131
import type { SentryTeam, Writer } from "../../types/index.js";
3232

3333
/** Command key for pagination cursor storage */
@@ -140,38 +140,6 @@ async function fetchAllOrgTeams(): Promise<TeamWithOrg[]> {
140140
return results;
141141
}
142142

143-
/**
144-
* Build a context key for pagination cursor validation.
145-
* Captures the org so cursors from different orgs are never mixed.
146-
*/
147-
function buildContextKey(org: string): string {
148-
return `host:${getApiBaseUrl()}|type:org:${org}`;
149-
}
150-
151-
/**
152-
* Resolve the cursor value from --cursor flag.
153-
* Handles the magic "last" value by looking up the cached cursor.
154-
*/
155-
function resolveCursor(
156-
cursorFlag: string | undefined,
157-
contextKey: string
158-
): string | undefined {
159-
if (!cursorFlag) {
160-
return;
161-
}
162-
if (cursorFlag === "last") {
163-
const cached = getPaginationCursor(PAGINATION_KEY, contextKey);
164-
if (!cached) {
165-
throw new ContextError(
166-
"Pagination cursor",
167-
"No saved cursor for this query. Run without --cursor first."
168-
);
169-
}
170-
return cached;
171-
}
172-
return cursorFlag;
173-
}
174-
175143
/** Build the CLI hint for fetching the next page. */
176144
function nextPageHint(org: string): string {
177145
return `sentry team list ${org}/ -c last`;
@@ -236,11 +204,6 @@ async function handleOrgAll(options: OrgAllOptions): Promise<void> {
236204
} else {
237205
stdout.write(`\nShowing ${teams.length} teams\n`);
238206
}
239-
240-
writeFooter(
241-
stdout,
242-
"Tip: Use 'sentry team list <org>/' for paginated results"
243-
);
244207
}
245208

246209
/**
@@ -431,8 +394,12 @@ export const listCommand = buildCommand({
431394
break;
432395

433396
case "org-all": {
434-
const contextKey = buildContextKey(parsed.org);
435-
const cursor = resolveCursor(flags.cursor, contextKey);
397+
const contextKey = buildOrgContextKey(parsed.org);
398+
const cursor = resolveOrgCursor(
399+
flags.cursor,
400+
PAGINATION_KEY,
401+
contextKey
402+
);
436403
await handleOrgAll({
437404
stdout,
438405
org: parsed.org,

src/lib/api-client.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1000,7 +1000,10 @@ export function listIssuesPaginated(
10001000
statsPeriod?: string;
10011001
} = {}
10021002
): Promise<PaginatedResponse<SentryIssue[]>> {
1003-
const projectFilter = `project:${projectSlug}`;
1003+
// Only add project filter when projectSlug is non-empty; an empty slug would
1004+
// produce "project:" (a truthy string that .filter(Boolean) won't remove),
1005+
// sending a malformed query to the API for org-wide listing.
1006+
const projectFilter = projectSlug ? `project:${projectSlug}` : "";
10041007
const fullQuery = [projectFilter, options.query].filter(Boolean).join(" ");
10051008

10061009
return orgScopedRequestPaginated<SentryIssue[]>(

src/lib/db/pagination.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@
55
* using a composite primary key so different contexts (e.g., different orgs)
66
* maintain independent cursors.
77
* Cursors expire after a short TTL to prevent stale pagination.
8+
*
9+
* Also exports shared helpers for building context keys and resolving cursor
10+
* flags, used by list commands that support cursor-based pagination.
811
*/
912

13+
import { ContextError } from "../errors.js";
14+
import { getApiBaseUrl } from "../sentry-client.js";
1015
import { getDatabase } from "./index.js";
1116
import { runUpsert } from "./utils.js";
1217

@@ -97,3 +102,49 @@ export function clearPaginationCursor(
97102
"DELETE FROM pagination_cursors WHERE command_key = ? AND context = ?"
98103
).run(commandKey, context);
99104
}
105+
106+
/**
107+
* Build a context key for org-scoped pagination cursor storage.
108+
*
109+
* Encodes the API base URL and org slug so cursors from different hosts or
110+
* orgs are never mixed up in the cursor cache.
111+
*
112+
* @param org - Organization slug
113+
* @returns Composite context key string
114+
*/
115+
export function buildOrgContextKey(org: string): string {
116+
return `host:${getApiBaseUrl()}|type:org:${org}`;
117+
}
118+
119+
/**
120+
* Resolve the cursor value from a `--cursor` flag.
121+
*
122+
* Handles the magic `"last"` value by looking up the cached cursor for the
123+
* given context key. Throws a {@link ContextError} if `"last"` is requested
124+
* but no cursor has been cached yet.
125+
*
126+
* @param cursorFlag - Raw value of the `--cursor` flag (undefined if not set)
127+
* @param commandKey - Command identifier used for cursor storage
128+
* @param contextKey - Serialized query context used for cursor storage
129+
* @returns Resolved cursor string, or `undefined` if no cursor was specified
130+
*/
131+
export function resolveOrgCursor(
132+
cursorFlag: string | undefined,
133+
commandKey: string,
134+
contextKey: string
135+
): string | undefined {
136+
if (!cursorFlag) {
137+
return;
138+
}
139+
if (cursorFlag === "last") {
140+
const cached = getPaginationCursor(commandKey, contextKey);
141+
if (!cached) {
142+
throw new ContextError(
143+
"Pagination cursor",
144+
"No saved cursor for this query. Run without --cursor first."
145+
);
146+
}
147+
return cached;
148+
}
149+
return cursorFlag;
150+
}

0 commit comments

Comments
 (0)