@@ -13,6 +13,12 @@ import {
1313 validateLimit ,
1414} from "../../lib/arg-parsing.js" ;
1515import { buildCommand } from "../../lib/command.js" ;
16+ import {
17+ buildPaginationContextKey ,
18+ clearPaginationCursor ,
19+ resolveOrgCursor ,
20+ setPaginationCursor ,
21+ } from "../../lib/db/pagination.js" ;
1622import { ContextError , ValidationError } from "../../lib/errors.js" ;
1723import {
1824 type FlatSpan ,
@@ -27,6 +33,7 @@ import {
2733 applyFreshFlag ,
2834 FRESH_ALIASES ,
2935 FRESH_FLAG ,
36+ LIST_CURSOR_FLAG ,
3037} from "../../lib/list-command.js" ;
3138import { logger } from "../../lib/logger.js" ;
3239import {
@@ -39,6 +46,7 @@ type ListFlags = {
3946 readonly limit : number ;
4047 readonly query ?: string ;
4148 readonly sort : SpanSortValue ;
49+ readonly cursor ?: string ;
4250 readonly json : boolean ;
4351 readonly fresh : boolean ;
4452 readonly fields ?: string [ ] ;
@@ -61,6 +69,9 @@ const DEFAULT_LIMIT = 25;
6169/** Default sort order for span results */
6270const DEFAULT_SORT : SpanSortValue = "date" ;
6371
72+ /** Pagination storage key for cursor resume */
73+ export const PAGINATION_KEY = "span-list" ;
74+
6475/** Usage hint for ContextError messages */
6576const USAGE_HINT = "sentry span list [<org>/<project>/]<trace-id>" ;
6677
@@ -111,7 +122,7 @@ export function parsePositionalArgs(args: string[]): {
111122 * Parse --limit flag, delegating range validation to shared utility.
112123 */
113124function parseLimit ( value : string ) : number {
114- return validateLimit ( value , 1 , MAX_LIMIT ) ; // min=1 is validateLimit's default, explicit for clarity
125+ return validateLimit ( value , 1 , MAX_LIMIT ) ;
115126}
116127
117128/**
@@ -128,6 +139,24 @@ export function parseSort(value: string): SpanSortValue {
128139 return value as SpanSortValue ;
129140}
130141
142+ /** Build the CLI hint for fetching the next page, preserving active flags. */
143+ function nextPageHint (
144+ org : string ,
145+ project : string ,
146+ traceId : string ,
147+ flags : Pick < ListFlags , "sort" | "query" >
148+ ) : string {
149+ const base = `sentry span list ${ org } /${ project } /${ traceId } -c last` ;
150+ const parts : string [ ] = [ ] ;
151+ if ( flags . sort !== DEFAULT_SORT ) {
152+ parts . push ( `--sort ${ flags . sort } ` ) ;
153+ }
154+ if ( flags . query ) {
155+ parts . push ( `-q "${ flags . query } "` ) ;
156+ }
157+ return parts . length > 0 ? `${ base } ${ parts . join ( " " ) } ` : base ;
158+ }
159+
131160// ---------------------------------------------------------------------------
132161// Output config types and formatters
133162// ---------------------------------------------------------------------------
@@ -138,6 +167,8 @@ type SpanListData = {
138167 flatSpans : FlatSpan [ ] ;
139168 /** Whether more results are available beyond the limit */
140169 hasMore : boolean ;
170+ /** Opaque cursor for fetching the next page (null/undefined when no more) */
171+ nextCursor ?: string | null ;
141172 /** The trace ID being queried */
142173 traceId : string ;
143174} ;
@@ -161,15 +192,26 @@ function formatSpanListHuman(data: SpanListData): string {
161192/**
162193 * Transform span list data for JSON output.
163194 *
164- * Produces a `{ data: [...], hasMore }` envelope matching the standard
165- * paginated list format. Applies `--fields` filtering per element.
195+ * Produces a `{ data: [...], hasMore, nextCursor? }` envelope matching the
196+ * standard paginated list format. Applies `--fields` filtering per element.
166197 */
167198function jsonTransformSpanList ( data : SpanListData , fields ?: string [ ] ) : unknown {
168199 const items =
169200 fields && fields . length > 0
170201 ? data . flatSpans . map ( ( item ) => filterFields ( item , fields ) )
171202 : data . flatSpans ;
172- return { data : items , hasMore : data . hasMore } ;
203+ const envelope : Record < string , unknown > = {
204+ data : items ,
205+ hasMore : data . hasMore ,
206+ } ;
207+ if (
208+ data . nextCursor !== null &&
209+ data . nextCursor !== undefined &&
210+ data . nextCursor !== ""
211+ ) {
212+ envelope . nextCursor = data . nextCursor ;
213+ }
214+ return envelope ;
173215}
174216
175217export const listCommand = buildCommand ( {
@@ -182,6 +224,8 @@ export const listCommand = buildCommand({
182224 " sentry span list <org>/<project>/<trace-id> # explicit org and project\n" +
183225 " sentry span list <project> <trace-id> # find project across all orgs\n\n" +
184226 "The trace ID is the 32-character hexadecimal identifier.\n\n" +
227+ "Pagination:\n" +
228+ " sentry span list <trace-id> -c last # fetch next page\n\n" +
185229 "Examples:\n" +
186230 " sentry span list <trace-id> # List spans in trace\n" +
187231 " sentry span list <trace-id> --limit 50 # Show more spans\n" +
@@ -223,13 +267,15 @@ export const listCommand = buildCommand({
223267 brief : `Sort order: ${ VALID_SORT_VALUES . join ( ", " ) } ` ,
224268 default : DEFAULT_SORT ,
225269 } ,
270+ cursor : LIST_CURSOR_FLAG ,
226271 fresh : FRESH_FLAG ,
227272 } ,
228273 aliases : {
229274 ...FRESH_ALIASES ,
230275 n : "limit" ,
231276 q : "query" ,
232277 s : "sort" ,
278+ c : "cursor" ,
233279 } ,
234280 } ,
235281 async * func ( this : SentryContext , flags : ListFlags , ...args : string [ ] ) {
@@ -288,6 +334,14 @@ export const listCommand = buildCommand({
288334 }
289335 const apiQuery = queryParts . join ( " " ) ;
290336
337+ // Build context key and resolve cursor for pagination
338+ const contextKey = buildPaginationContextKey (
339+ "span" ,
340+ `${ target . org } /${ target . project } /${ traceId } ` ,
341+ { sort : flags . sort , q : flags . query }
342+ ) ;
343+ const cursor = resolveOrgCursor ( flags . cursor , PAGINATION_KEY , contextKey ) ;
344+
291345 // Fetch spans from EAP endpoint
292346 const { data : spanItems , nextCursor } = await listSpans (
293347 target . org ,
@@ -296,22 +350,32 @@ export const listCommand = buildCommand({
296350 query : apiQuery ,
297351 sort : flags . sort ,
298352 limit : flags . limit ,
353+ cursor,
299354 }
300355 ) ;
301356
357+ // Store or clear pagination cursor
358+ if ( nextCursor ) {
359+ setPaginationCursor ( PAGINATION_KEY , contextKey , nextCursor ) ;
360+ } else {
361+ clearPaginationCursor ( PAGINATION_KEY , contextKey ) ;
362+ }
363+
302364 const flatSpans = spanItems . map ( spanListItemToFlatSpan ) ;
303- const hasMore = nextCursor !== undefined ;
365+ const hasMore = ! ! nextCursor ;
304366
305367 // Build hint footer
306368 let hint : string | undefined ;
307- if ( flatSpans . length > 0 ) {
369+ if ( flatSpans . length === 0 && hasMore ) {
370+ hint = `Try the next page: ${ nextPageHint ( target . org , target . project , traceId , flags ) } ` ;
371+ } else if ( flatSpans . length > 0 ) {
308372 const countText = `Showing ${ flatSpans . length } span${ flatSpans . length === 1 ? "" : "s" } .` ;
309373 hint = hasMore
310- ? `${ countText } Use --limit to see more. `
374+ ? `${ countText } Next page: ${ nextPageHint ( target . org , target . project , traceId , flags ) } `
311375 : `${ countText } Use 'sentry span view ${ traceId } <span-id>' to view span details.` ;
312376 }
313377
314- yield new CommandOutput ( { flatSpans, hasMore, traceId } ) ;
378+ yield new CommandOutput ( { flatSpans, hasMore, nextCursor , traceId } ) ;
315379 return { hint } ;
316380 } ,
317381} ) ;
0 commit comments