33 *
44 * List and stream logs from Sentry projects.
55 * Supports real-time streaming with --follow flag.
6- * Supports -- trace flag to filter logs by trace ID .
6+ * Supports trace ID as a positional argument to filter logs by trace.
77 */
88
99// biome-ignore lint/performance/noNamespaceImport: Sentry SDK recommends namespace import
1010import * as Sentry from "@sentry/bun" ;
1111import type { SentryContext } from "../../context.js" ;
1212import { listLogs , listTraceLogs } from "../../lib/api-client.js" ;
1313import { validateLimit } from "../../lib/arg-parsing.js" ;
14- import { AuthError , ContextError , stringifyUnknown } from "../../lib/errors.js" ;
14+ import { AuthError , stringifyUnknown } from "../../lib/errors.js" ;
1515import {
1616 buildLogRowCells ,
1717 createLogStreamingTable ,
@@ -34,19 +34,23 @@ import {
3434 TARGET_PATTERN_NOTE ,
3535} from "../../lib/list-command.js" ;
3636import { logger } from "../../lib/logger.js" ;
37+ import { withProgress } from "../../lib/polling.js" ;
38+ import { resolveOrgProjectFromArg } from "../../lib/resolve-target.js" ;
39+ import { isTraceId } from "../../lib/trace-id.js" ;
3740import {
38- resolveOrg ,
39- resolveOrgProjectFromArg ,
40- } from "../../lib/resolve-target.js" ;
41- import { validateTraceId } from "../../lib/trace-id.js" ;
41+ type ParsedTraceTarget ,
42+ parseTraceTarget ,
43+ resolveTraceOrg ,
44+ warnIfNormalized ,
45+ } from "../../lib/trace-target.js" ;
4246import { getUpdateNotification } from "../../lib/version-check.js" ;
4347
4448type ListFlags = {
4549 readonly limit : number ;
4650 readonly query ?: string ;
4751 readonly follow ?: number ;
52+ readonly period : string ;
4853 readonly json : boolean ;
49- readonly trace ?: string ;
5054 readonly fresh : boolean ;
5155 readonly fields ?: string [ ] ;
5256} ;
@@ -62,6 +66,8 @@ type LogListResult = {
6266 logs : LogLike [ ] ;
6367 /** Trace ID, present for trace-filtered queries */
6468 traceId ?: string ;
69+ /** Whether more results are available beyond the limit */
70+ hasMore : boolean ;
6571} ;
6672
6773/** Output yielded by log list: either a batch (single-fetch) or an individual item (follow). */
@@ -82,6 +88,12 @@ const DEFAULT_POLL_INTERVAL = 2;
8288/** Command name used in resolver error messages */
8389const COMMAND_NAME = "log list" ;
8490
91+ /** Usage hint for trace mode error messages */
92+ const TRACE_USAGE_HINT = "sentry log list [<org>/]<trace-id>" ;
93+
94+ /** Default time period for trace-logs queries */
95+ const DEFAULT_TRACE_PERIOD = "14d" ;
96+
8597/**
8698 * Parse --limit flag, delegating range validation to shared utility.
8799 */
@@ -130,6 +142,56 @@ type FetchResult = {
130142 hint : string ;
131143} ;
132144
145+ // ---------------------------------------------------------------------------
146+ // Positional argument disambiguation
147+ // ---------------------------------------------------------------------------
148+
149+ /**
150+ * Parsed result from log list positional arguments.
151+ *
152+ * Discriminated on `mode`:
153+ * - `"project"` — standard project-scoped log listing (existing path)
154+ * - `"trace"` — trace-filtered log listing via trace-logs endpoint
155+ */
156+ type ParsedLogArgs =
157+ | { mode : "project" ; target ?: string }
158+ | { mode : "trace" ; parsed : ParsedTraceTarget } ;
159+
160+ /**
161+ * Disambiguate log list positional arguments.
162+ *
163+ * Checks if the "tail" segment (last part after `/`, or the entire arg
164+ * if no `/`) looks like a 32-char hex trace ID. If so, delegates to
165+ * {@link parseTraceTarget} for full trace target parsing. Otherwise,
166+ * treats the argument as a project target.
167+ *
168+ * @param args - Positional arguments from CLI
169+ * @returns Parsed args with mode discrimination
170+ */
171+ function parseLogListArgs ( args : string [ ] ) : ParsedLogArgs {
172+ if ( args . length === 0 ) {
173+ return { mode : "project" } ;
174+ }
175+
176+ const first = args [ 0 ] ;
177+ if ( first === undefined ) {
178+ return { mode : "project" } ;
179+ }
180+
181+ // Check the tail segment: last part after `/`, or the entire arg
182+ const lastSlash = first . lastIndexOf ( "/" ) ;
183+ const tail = lastSlash === - 1 ? first : first . slice ( lastSlash + 1 ) ;
184+
185+ if ( isTraceId ( tail ) ) {
186+ return {
187+ mode : "trace" ,
188+ parsed : parseTraceTarget ( args , TRACE_USAGE_HINT ) ,
189+ } ;
190+ }
191+
192+ return { mode : "project" , target : first } ;
193+ }
194+
133195/**
134196 * Execute a single fetch of logs (non-streaming mode).
135197 *
@@ -144,11 +206,11 @@ async function executeSingleFetch(
144206 const logs = await listLogs ( org , project , {
145207 query : flags . query ,
146208 limit : flags . limit ,
147- statsPeriod : "90d" ,
209+ statsPeriod : flags . period ,
148210 } ) ;
149211
150212 if ( logs . length === 0 ) {
151- return { result : { logs : [ ] } , hint : "No logs found." } ;
213+ return { result : { logs : [ ] , hasMore : false } , hint : "No logs found." } ;
152214 }
153215
154216 // Reverse for chronological order (API returns newest first, tail shows oldest first)
@@ -158,7 +220,10 @@ async function executeSingleFetch(
158220 const countText = `Showing ${ logs . length } log${ logs . length === 1 ? "" : "s" } .` ;
159221 const tip = hasMore ? " Use --limit to show more, or -f to follow." : "" ;
160222
161- return { result : { logs : chronological } , hint : `${ countText } ${ tip } ` } ;
223+ return {
224+ result : { logs : chronological , hasMore } ,
225+ hint : `${ countText } ${ tip } ` ,
226+ } ;
162227}
163228
164229// ---------------------------------------------------------------------------
@@ -368,7 +433,11 @@ async function* yieldTraceFollowItems<T extends LogLike>(
368433 for await ( const batch of generator ) {
369434 if ( ! contextSent && batch . length > 0 ) {
370435 // First non-empty batch: yield as LogListResult to set trace context
371- yield new CommandOutput < LogOutput > ( { logs : batch , traceId } ) ;
436+ yield new CommandOutput < LogOutput > ( {
437+ logs : batch ,
438+ traceId,
439+ hasMore : false ,
440+ } ) ;
372441 contextSent = true ;
373442 } else {
374443 for ( const item of batch ) {
@@ -378,11 +447,8 @@ async function* yieldTraceFollowItems<T extends LogLike>(
378447 }
379448}
380449
381- /** Default time period for trace-logs queries */
382- const DEFAULT_TRACE_PERIOD = "14d" ;
383-
384450/**
385- * Execute a single fetch of trace-filtered logs (non-streaming, -- trace mode).
451+ * Execute a single fetch of trace-filtered logs (non-streaming, trace mode).
386452 * Uses the dedicated trace-logs endpoint which is org-scoped.
387453 *
388454 * Returns the fetched logs, trace ID, and a human-readable hint.
@@ -393,17 +459,21 @@ async function executeTraceSingleFetch(
393459 traceId : string ,
394460 flags : ListFlags
395461) : Promise < FetchResult > {
462+ // In trace mode, use a shorter default period if the user hasn't
463+ // explicitly changed it from the command-level default of "90d".
464+ const period = flags . period === "90d" ? DEFAULT_TRACE_PERIOD : flags . period ;
465+
396466 const logs = await listTraceLogs ( org , traceId , {
397467 query : flags . query ,
398468 limit : flags . limit ,
399- statsPeriod : DEFAULT_TRACE_PERIOD ,
469+ statsPeriod : period ,
400470 } ) ;
401471
402472 if ( logs . length === 0 ) {
403473 return {
404- result : { logs : [ ] , traceId } ,
474+ result : { logs : [ ] , traceId, hasMore : false } ,
405475 hint :
406- `No logs found for trace ${ traceId } in the last ${ DEFAULT_TRACE_PERIOD } .\n\n` +
476+ `No logs found for trace ${ traceId } in the last ${ period } .\n\n` +
407477 "Try 'sentry trace logs' for more options (e.g., --period 30d)." ,
408478 } ;
409479 }
@@ -415,7 +485,7 @@ async function executeTraceSingleFetch(
415485 const tip = hasMore ? " Use --limit to show more." : "" ;
416486
417487 return {
418- result : { logs : chronological , traceId } ,
488+ result : { logs : chronological , traceId, hasMore } ,
419489 hint : `${ countText } ${ tip } ` ,
420490 } ;
421491}
@@ -518,16 +588,18 @@ function createLogRenderer(): HumanRenderer<LogOutput> {
518588 * Transform log output into the JSON shape.
519589 *
520590 * Discriminates between {@link LogListResult} (single-fetch) and bare
521- * {@link LogLike} items (follow mode). Single-fetch yields a JSON array;
522- * follow mode yields one JSON object per line (JSONL).
591+ * {@link LogLike} items (follow mode). Single-fetch yields a JSON envelope
592+ * with `data` and `hasMore`; follow mode yields one JSON object per line (JSONL).
523593 */
524594function jsonTransformLogOutput ( data : LogOutput , fields ?: string [ ] ) : unknown {
525595 if ( "logs" in data && Array . isArray ( ( data as LogListResult ) . logs ) ) {
526- // Batch (single-fetch): return array
527- const logs = ( data as LogListResult ) . logs ;
528- return fields && fields . length > 0
529- ? logs . map ( ( log ) => filterFields ( log , fields ) )
530- : logs ;
596+ // Batch (single-fetch): return envelope with data + hasMore
597+ const logList = data as LogListResult ;
598+ const items =
599+ fields && fields . length > 0
600+ ? logList . logs . map ( ( log ) => filterFields ( log , fields ) )
601+ : logList . logs ;
602+ return { data : items , hasMore : logList . hasMore } ;
531603 }
532604 // Single item (follow mode): return bare object for JSONL
533605 return fields && fields . length > 0 ? filterFields ( data , fields ) : data ;
@@ -544,16 +616,15 @@ export const listCommand = buildListCommand("log", {
544616 " sentry log list <project> # find project across all orgs\n\n" +
545617 `${ TARGET_PATTERN_NOTE } \n\n` +
546618 "Trace filtering:\n" +
547- " When --trace is given, only org resolution is needed (the trace-logs\n" +
548- " endpoint is org-scoped). The positional target is treated as an org\n" +
549- " slug, not an org/project pair.\n\n" +
619+ " sentry log list <trace-id> # Filter by trace (auto-detect org)\n" +
620+ " sentry log list <org>/<trace-id> # Filter by trace (explicit org)\n\n" +
550621 "Examples:\n" +
551622 " sentry log list # List last 100 logs\n" +
552623 " sentry log list -f # Stream logs (2s poll interval)\n" +
553624 " sentry log list -f 5 # Stream logs (5s poll interval)\n" +
554625 " sentry log list --limit 50 # Show last 50 logs\n" +
555626 " sentry log list -q 'level:error' # Filter to errors only\n" +
556- " sentry log list --trace abc123def456abc123def456abc123de # Filter by trace\n\n" +
627+ " sentry log list abc123def456abc123def456abc123de # Filter by trace\n\n" +
557628 "Alias: `sentry logs` → `sentry log list`" ,
558629 } ,
559630 output : {
@@ -562,15 +633,12 @@ export const listCommand = buildListCommand("log", {
562633 } ,
563634 parameters : {
564635 positional : {
565- kind : "tuple" ,
566- parameters : [
567- {
568- placeholder : "org/project" ,
569- brief : "<org>/<project> or <project> (search)" ,
570- parse : String ,
571- optional : true ,
572- } ,
573- ] ,
636+ kind : "array" ,
637+ parameter : {
638+ placeholder : "org/project-or-trace-id" ,
639+ brief : "[<org>/[<project>/]]<trace-id>, <org>/<project>, or <project>" ,
640+ parse : String ,
641+ } ,
574642 } ,
575643 flags : {
576644 limit : {
@@ -592,43 +660,38 @@ export const listCommand = buildListCommand("log", {
592660 optional : true ,
593661 inferEmpty : true ,
594662 } ,
595- trace : {
663+ period : {
596664 kind : "parsed" ,
597- parse : validateTraceId ,
598- brief : "Filter logs by trace ID (32-character hex string)" ,
599- optional : true ,
665+ parse : String ,
666+ brief : 'Time period (e.g., "90d", "14d", "24h")' ,
667+ default : "90d" ,
600668 } ,
601669 fresh : FRESH_FLAG ,
602670 } ,
603671 aliases : {
604672 n : "limit" ,
605673 q : "query" ,
606674 f : "follow" ,
675+ t : "period" ,
607676 } ,
608677 } ,
609- async * func ( this : SentryContext , flags : ListFlags , target ? : string ) {
678+ async * func ( this : SentryContext , flags : ListFlags , ... args : string [ ] ) {
610679 applyFreshFlag ( flags ) ;
611680 const { cwd, setContext } = this ;
612681
613- if ( flags . trace ) {
682+ const parsed = parseLogListArgs ( args ) ;
683+
684+ if ( parsed . mode === "trace" ) {
614685 // Trace mode: use the org-scoped trace-logs endpoint.
615- // The positional target is treated as an org slug (not org/project).
616- const resolved = await resolveOrg ( {
617- org : target ,
686+ warnIfNormalized ( parsed . parsed , "log.list" ) ;
687+ const { traceId , org } = await resolveTraceOrg (
688+ parsed . parsed ,
618689 cwd ,
619- } ) ;
620- if ( ! resolved ) {
621- throw new ContextError ( "Organization" , "sentry log list --trace <id>" , [
622- "Set a default org with 'sentry org list', or specify one explicitly" ,
623- `Example: sentry log list myorg --trace ${ flags . trace } ` ,
624- ] ) ;
625- }
626- const { org } = resolved ;
690+ TRACE_USAGE_HINT
691+ ) ;
627692 setContext ( [ org ] , [ ] ) ;
628693
629694 if ( flags . follow ) {
630- const traceId = flags . trace ;
631-
632695 // Banner (suppressed in JSON mode)
633696 writeFollowBanner (
634697 flags . follow ?? DEFAULT_POLL_INTERVAL ,
@@ -676,20 +739,18 @@ export const listCommand = buildListCommand("log", {
676739 return ;
677740 }
678741
679- const { result, hint } = await executeTraceSingleFetch (
680- org ,
681- flags . trace ,
682- flags
742+ const { result, hint } = await withProgress (
743+ { message : "Fetching logs..." } ,
744+ ( ) => executeTraceSingleFetch ( org , traceId , flags )
683745 ) ;
684746 yield new CommandOutput ( result ) ;
685747 return { hint } ;
686748 }
687749
688- // Standard project-scoped mode — kept in else-like block to avoid
689- // `org` shadowing the trace-mode `org` declaration above.
750+ // Standard project-scoped mode
690751 {
691752 const { org, project } = await resolveOrgProjectFromArg (
692- target ,
753+ parsed . target ,
693754 cwd ,
694755 COMMAND_NAME
695756 ) ;
@@ -719,7 +780,10 @@ export const listCommand = buildListCommand("log", {
719780 return ;
720781 }
721782
722- const { result, hint } = await executeSingleFetch ( org , project , flags ) ;
783+ const { result, hint } = await withProgress (
784+ { message : "Fetching logs..." } ,
785+ ( ) => executeSingleFetch ( org , project , flags )
786+ ) ;
723787 yield new CommandOutput ( result ) ;
724788 return { hint } ;
725789 }
0 commit comments