11/**
22 * sentry dashboard view
33 *
4- * View details of a specific dashboard.
4+ * View a dashboard with rendered widget data (sparklines, tables, big numbers).
5+ * Supports --refresh for auto-refreshing live display.
56 */
67
78import type { SentryContext } from "../../context.js" ;
8- import { getDashboard } from "../../lib/api-client.js" ;
9+ import { getDashboard , queryAllWidgets } from "../../lib/api-client.js" ;
910import { parseOrgProjectArg } from "../../lib/arg-parsing.js" ;
1011import { openInBrowser } from "../../lib/browser.js" ;
1112import { buildCommand } from "../../lib/command.js" ;
12- import { formatDashboardView } from "../../lib/formatters/human.js" ;
13+ import type { DashboardViewData } from "../../lib/formatters/dashboard.js" ;
14+ import {
15+ createDashboardViewRenderer ,
16+ transformDashboardViewJson ,
17+ } from "../../lib/formatters/dashboard.js" ;
1318import { CommandOutput } from "../../lib/formatters/output.js" ;
1419import {
1520 applyFreshFlag ,
1621 FRESH_ALIASES ,
1722 FRESH_FLAG ,
1823} from "../../lib/list-command.js" ;
24+ import { logger } from "../../lib/logger.js" ;
1925import { withProgress } from "../../lib/polling.js" ;
26+ import { resolveOrgRegion } from "../../lib/region.js" ;
2027import { buildDashboardUrl } from "../../lib/sentry-urls.js" ;
21- import type { DashboardDetail } from "../../types/dashboard.js" ;
28+ import type {
29+ DashboardWidget ,
30+ WidgetDataResult ,
31+ } from "../../types/dashboard.js" ;
2232import {
2333 parseDashboardPositionalArgs ,
2434 resolveDashboardId ,
2535 resolveOrgFromTarget ,
2636} from "./resolve.js" ;
2737
38+ /** Default auto-refresh interval in seconds */
39+ const DEFAULT_REFRESH_INTERVAL = 60 ;
40+
41+ /** Minimum auto-refresh interval in seconds (avoid rate limiting) */
42+ const MIN_REFRESH_INTERVAL = 10 ;
43+
2844type ViewFlags = {
2945 readonly web : boolean ;
3046 readonly fresh : boolean ;
47+ readonly refresh ?: number ;
48+ readonly period ?: string ;
3149 readonly json : boolean ;
3250 readonly fields ?: string [ ] ;
3351} ;
3452
35- type ViewResult = DashboardDetail & { url : string } ;
53+ /**
54+ * Parse --refresh flag value.
55+ * Supports: -r (empty string → 60s default), -r 30 (explicit interval in seconds)
56+ */
57+ function parseRefresh ( value : string ) : number {
58+ if ( value === "" ) {
59+ return DEFAULT_REFRESH_INTERVAL ;
60+ }
61+ const num = Number . parseInt ( value , 10 ) ;
62+ if ( Number . isNaN ( num ) || num < MIN_REFRESH_INTERVAL ) {
63+ throw new Error (
64+ `--refresh interval must be at least ${ MIN_REFRESH_INTERVAL } seconds`
65+ ) ;
66+ }
67+ return num ;
68+ }
69+
70+ /**
71+ * Sleep that resolves early when an AbortSignal fires.
72+ * Resolves (not rejects) on abort for clean generator shutdown.
73+ */
74+ function abortableSleep ( ms : number , signal : AbortSignal ) : Promise < void > {
75+ return new Promise < void > ( ( resolve ) => {
76+ if ( signal . aborted ) {
77+ resolve ( ) ;
78+ return ;
79+ }
80+ const onAbort = ( ) => {
81+ clearTimeout ( timer ) ;
82+ resolve ( ) ;
83+ } ;
84+ const timer = setTimeout ( ( ) => {
85+ signal . removeEventListener ( "abort" , onAbort ) ;
86+ resolve ( ) ;
87+ } , ms ) ;
88+ signal . addEventListener ( "abort" , onAbort , { once : true } ) ;
89+ } ) ;
90+ }
91+
92+ /**
93+ * Build the DashboardViewData from a dashboard and its widget query results.
94+ */
95+ function buildViewData (
96+ dashboard : {
97+ id : string ;
98+ title : string ;
99+ dateCreated ?: string ;
100+ environment ?: string [ ] ;
101+ } ,
102+ widgetResults : Map < number , WidgetDataResult > ,
103+ widgets : DashboardWidget [ ] ,
104+ opts : { period : string ; url : string }
105+ ) : DashboardViewData {
106+ return {
107+ id : dashboard . id ,
108+ title : dashboard . title ,
109+ period : opts . period ,
110+ fetchedAt : new Date ( ) . toISOString ( ) ,
111+ url : opts . url ,
112+ dateCreated : dashboard . dateCreated ,
113+ environment : dashboard . environment ,
114+ widgets : widgets . map ( ( w , i ) => ( {
115+ title : w . title ,
116+ displayType : w . displayType ,
117+ widgetType : w . widgetType ,
118+ layout : w . layout ,
119+ queries : w . queries ,
120+ data : widgetResults . get ( i ) ?? {
121+ type : "error" as const ,
122+ message : "No data returned" ,
123+ } ,
124+ } ) ) ,
125+ } ;
126+ }
36127
37128export const viewCommand = buildCommand ( {
38129 docs : {
39130 brief : "View a dashboard" ,
40131 fullDescription :
41- "View details of a specific Sentry dashboard.\n\n" +
132+ "View a Sentry dashboard with rendered widget data.\n\n" +
133+ "Fetches actual data for each widget and displays sparkline charts,\n" +
134+ "tables, and big numbers in the terminal.\n\n" +
42135 "The dashboard can be specified by numeric ID or title.\n\n" +
43136 "Examples:\n" +
44137 " sentry dashboard view 12345\n" +
45138 " sentry dashboard view 'My Dashboard'\n" +
46139 " sentry dashboard view my-org/ 12345\n" +
47140 " sentry dashboard view 12345 --json\n" +
141+ " sentry dashboard view 12345 --period 7d\n" +
142+ " sentry dashboard view 12345 -r\n" +
143+ " sentry dashboard view 12345 -r 30\n" +
48144 " sentry dashboard view 12345 --web" ,
49145 } ,
50146 output : {
51- human : formatDashboardView ,
147+ human : createDashboardViewRenderer ,
148+ jsonTransform : transformDashboardViewJson ,
52149 } ,
53150 parameters : {
54151 positional : {
@@ -65,8 +162,21 @@ export const viewCommand = buildCommand({
65162 default : false ,
66163 } ,
67164 fresh : FRESH_FLAG ,
165+ refresh : {
166+ kind : "parsed" ,
167+ parse : parseRefresh ,
168+ brief : "Auto-refresh interval in seconds (default: 60, min: 10)" ,
169+ optional : true ,
170+ inferEmpty : true ,
171+ } ,
172+ period : {
173+ kind : "parsed" ,
174+ parse : String ,
175+ brief : 'Time period override (e.g., "24h", "7d", "14d")' ,
176+ optional : true ,
177+ } ,
68178 } ,
69- aliases : { ...FRESH_ALIASES , w : "web" } ,
179+ aliases : { ...FRESH_ALIASES , w : "web" , r : "refresh" , t : "period" } ,
70180 } ,
71181 async * func ( this : SentryContext , flags : ViewFlags , ...args : string [ ] ) {
72182 applyFreshFlag ( flags ) ;
@@ -80,20 +190,66 @@ export const viewCommand = buildCommand({
80190 "sentry dashboard view <org>/ <id>"
81191 ) ;
82192 const dashboardId = await resolveDashboardId ( orgSlug , dashboardRef ) ;
83-
84193 const url = buildDashboardUrl ( orgSlug , dashboardId ) ;
85194
86195 if ( flags . web ) {
87196 await openInBrowser ( url , "dashboard" ) ;
88197 return ;
89198 }
90199
200+ // Fetch the dashboard definition (widget structure)
91201 const dashboard = await withProgress (
92202 { message : "Fetching dashboard..." , json : flags . json } ,
93203 ( ) => getDashboard ( orgSlug , dashboardId )
94204 ) ;
95205
96- yield new CommandOutput ( { ...dashboard , url } as ViewResult ) ;
206+ const regionUrl = await resolveOrgRegion ( orgSlug ) ;
207+ const period = flags . period ?? dashboard . period ?? "24h" ;
208+ const widgets = dashboard . widgets ?? [ ] ;
209+
210+ if ( flags . refresh !== undefined ) {
211+ // ── Refresh mode: poll and re-render ──
212+ const interval = flags . refresh ;
213+ if ( ! flags . json ) {
214+ logger . info (
215+ `Auto-refreshing dashboard every ${ interval } s. Press Ctrl+C to stop.`
216+ ) ;
217+ }
218+
219+ const controller = new AbortController ( ) ;
220+ const stop = ( ) => controller . abort ( ) ;
221+ process . once ( "SIGINT" , stop ) ;
222+
223+ try {
224+ while ( ! controller . signal . aborted ) {
225+ const widgetData = await queryAllWidgets (
226+ regionUrl ,
227+ orgSlug ,
228+ dashboard ,
229+ { period }
230+ ) ;
231+
232+ yield new CommandOutput (
233+ buildViewData ( dashboard , widgetData , widgets , { period, url } )
234+ ) ;
235+
236+ await abortableSleep ( interval * 1000 , controller . signal ) ;
237+ }
238+ } finally {
239+ process . removeListener ( "SIGINT" , stop ) ;
240+ }
241+ return ;
242+ }
243+
244+ // ── Single fetch mode ──
245+ const widgetData = await withProgress (
246+ { message : "Querying widget data..." , json : flags . json } ,
247+ ( ) => queryAllWidgets ( regionUrl , orgSlug , dashboard , { period } )
248+ ) ;
249+
250+ yield new CommandOutput (
251+ buildViewData ( dashboard , widgetData , widgets , { period, url } )
252+ ) ;
97253 return { hint : `Dashboard: ${ url } ` } ;
98254 } ,
99255} ) ;
0 commit comments