@@ -4,8 +4,19 @@ import {hux} from '@heroku/heroku-cli-util'
44import { capitalize } from '@oclif/core/lib/util'
55import { formatDistanceToNow } from 'date-fns'
66import HTTP from '@heroku/http-call'
7+ import {
8+ TrustInstance ,
9+ TrustIncident ,
10+ TrustMaintenance ,
11+ HerokuStatus ,
12+ FormattedTrustStatus ,
13+ SystemStatus ,
14+ Localization ,
15+ } from '../lib/types/status'
716
8- import { maxBy } from '../lib/status/util'
17+ import { getMaxUpdateTypeLength } from '../lib/status/util'
18+
19+ const errorMessage = 'Heroku platform status is unavailable at this time. Refer to https://status.salesforce.com/products/Heroku or try again later.'
920
1021const printStatus = ( status : string ) => {
1122 const colorize = ( color as any ) [ status ]
@@ -18,6 +29,106 @@ const printStatus = (status: string) => {
1829 return colorize ( message )
1930}
2031
32+ const getTrustStatus = async ( ) => {
33+ const trustHost = process . env . SF_TRUST_STAGING ? 'https://status-api-stg.test.edgekey.net/v1' : 'https://api.status.salesforce.com/v1'
34+ const currentDateTime = new Date ( Date . now ( ) ) . toISOString ( )
35+ let instances : TrustInstance [ ] = [ ]
36+ let activeIncidents : TrustIncident [ ] = [ ]
37+ let maintenances : TrustMaintenance [ ] = [ ]
38+ let localizations : Localization [ ] = [ ]
39+
40+ try {
41+ const [ instanceResponse , activeIncidentsResponse , maintenancesResponse , localizationsResponse ] = await Promise . all ( [
42+ HTTP . get < TrustInstance [ ] > ( `${ trustHost } /instances?products=Heroku` ) ,
43+ HTTP . get < TrustIncident [ ] > ( `${ trustHost } /incidents/active` ) ,
44+ HTTP . get < TrustMaintenance [ ] > ( `${ trustHost } /maintenances?startTime=${ currentDateTime } &limit=10&offset=0&product=Heroku&locale=en` ) ,
45+ HTTP . get < Localization [ ] > ( `${ trustHost } /localizations?locale=en` ) ,
46+ ] )
47+ instances = instanceResponse . body
48+ activeIncidents = activeIncidentsResponse . body
49+ maintenances = maintenancesResponse . body
50+ localizations = localizationsResponse . body
51+ } catch {
52+ ux . error ( errorMessage , { exit : 1 } )
53+ }
54+
55+ return formatTrustResponse ( instances , activeIncidents , maintenances , localizations )
56+ }
57+
58+ const determineIncidentSeverity = ( incidents : TrustIncident [ ] ) => {
59+ const severityArray : string [ ] = [ ]
60+ incidents . forEach ( incident => {
61+ incident . IncidentImpacts . forEach ( impact => {
62+ if ( ! impact . endTime && impact . severity ) {
63+ severityArray . push ( impact . severity )
64+ }
65+ } )
66+ } )
67+ if ( severityArray . includes ( 'major' ) ) return 'red'
68+ if ( severityArray . includes ( 'minor' ) ) return 'yellow'
69+ return 'green'
70+ }
71+
72+ const formatTrustResponse = ( instances : TrustInstance [ ] , activeIncidents : TrustIncident [ ] , maintenances : TrustMaintenance [ ] , localizations : Localization [ ] ) : FormattedTrustStatus => {
73+ const systemStatus : SystemStatus [ ] = [ ]
74+ const incidents : TrustIncident [ ] = [ ]
75+ const scheduled : TrustMaintenance [ ] = [ ]
76+ const instanceKeyArray = new Set ( instances . map ( instance => instance . key ) )
77+ const herokuActiveIncidents = activeIncidents . filter ( incident => {
78+ return incident . instanceKeys . some ( key => instanceKeyArray . has ( key ) )
79+ } )
80+ const toolsIncidents = herokuActiveIncidents . filter ( incident => {
81+ const tools = [ 'TOOLS' , 'Tools' , 'CLI' , 'Dashboard' , 'Platform API' ]
82+ return tools . some ( tool => incident . serviceKeys . includes ( tool ) )
83+ } )
84+ const appsIncidents = herokuActiveIncidents . filter ( incident => {
85+ return incident . serviceKeys . includes ( 'HerokuApps' ) || incident . serviceKeys . includes ( 'Apps' )
86+ } )
87+ const dataIncidents = herokuActiveIncidents . filter ( incident => {
88+ return incident . serviceKeys . includes ( 'HerokuData' ) || incident . serviceKeys . includes ( 'Data' )
89+ } )
90+
91+ if ( appsIncidents . length > 0 ) {
92+ const severity = determineIncidentSeverity ( appsIncidents )
93+ systemStatus . push ( { system : 'Apps' , status : severity } )
94+ incidents . push ( ...appsIncidents )
95+ } else {
96+ systemStatus . push ( { system : 'Apps' , status : 'green' } )
97+ }
98+
99+ if ( dataIncidents . length > 0 ) {
100+ const severity = determineIncidentSeverity ( dataIncidents )
101+ systemStatus . push ( { system : 'Data' , status : severity } )
102+ incidents . push ( ...dataIncidents )
103+ } else {
104+ systemStatus . push ( { system : 'Data' , status : 'green' } )
105+ }
106+
107+ if ( toolsIncidents . length > 0 ) {
108+ const severity = determineIncidentSeverity ( toolsIncidents )
109+ systemStatus . push ( { system : 'Tools' , status : severity } )
110+ incidents . push ( ...toolsIncidents )
111+ } else {
112+ systemStatus . push ( { system : 'Tools' , status : 'green' } )
113+ }
114+
115+ if ( maintenances . length > 0 ) scheduled . push ( ...maintenances )
116+
117+ if ( incidents . length > 0 ) {
118+ incidents . forEach ( incident => {
119+ incident . IncidentEvents . forEach ( event => {
120+ event . localizedType = localizations . find ( ( l : any ) => l . modelKey === event . type ) ?. text
121+ } )
122+ } )
123+ }
124+
125+ return {
126+ status : systemStatus ,
127+ incidents,
128+ scheduled,
129+ }
130+ }
131+
21132export default class Status extends Command {
22133 static description = 'display current status of the Heroku platform'
23134
@@ -27,30 +138,65 @@ export default class Status extends Command {
27138
28139 async run ( ) {
29140 const { flags} = await this . parse ( Status )
30- const apiPath = '/api/v4/current-status'
141+ const herokuApiPath = '/api/v4/current-status'
142+ let herokuStatus
143+ let formattedTrustStatus
31144
32- const host = process . env . HEROKU_STATUS_HOST || 'https://status.heroku.com'
33- const { body} = await HTTP . get < any > ( host + apiPath )
145+ if ( process . env . TRUST_ONLY ) {
146+ formattedTrustStatus = await getTrustStatus ( )
147+ } else {
148+ try {
149+ // Try calling the Heroku status API first
150+ const herokuHost = process . env . HEROKU_STATUS_HOST || 'https://status.heroku.com'
151+ const herokuStatusResponse = await HTTP . get < HerokuStatus > ( herokuHost + herokuApiPath )
152+ herokuStatus = herokuStatusResponse . body
153+ } catch {
154+ // If the Heroku status API call fails, call the SF Trust API
155+ formattedTrustStatus = await getTrustStatus ( )
156+ }
157+ }
158+
159+ if ( ! herokuStatus && ! formattedTrustStatus ) ux . error ( errorMessage , { exit : 1 } )
34160
35161 if ( flags . json ) {
36- hux . styledJSON ( body )
162+ hux . styledJSON ( herokuStatus ?? formattedTrustStatus )
37163 return
38164 }
39165
40- for ( const item of body . status ) {
41- const message = printStatus ( item . status )
166+ const systemStatus = herokuStatus ? herokuStatus . status : formattedTrustStatus ?. status
167+
168+ if ( systemStatus ) {
169+ for ( const item of systemStatus ) {
170+ const message = printStatus ( item . status )
42171
43- this . log ( `${ ( item . system + ':' ) . padEnd ( 11 ) } ${ message } ` )
172+ this . log ( `${ ( item . system + ':' ) . padEnd ( 11 ) } ${ message } ` )
173+ }
174+ } else {
175+ ux . error ( errorMessage , { exit : 1 } )
44176 }
45177
46- for ( const incident of body . incidents ) {
47- ux . log ( )
48- hux . styledHeader ( `${ incident . title } ${ color . yellow ( incident . created_at ) } ${ color . cyan ( incident . full_url ) } ` )
178+ if ( herokuStatus ) {
179+ for ( const incident of herokuStatus . incidents ) {
180+ ux . log ( )
181+ hux . styledHeader ( `${ incident . title } ${ color . yellow ( incident . created_at ) } ${ color . cyan ( incident . full_url ) } ` )
182+
183+ const padding = getMaxUpdateTypeLength ( incident . updates . map ( update => update . update_type ) )
184+ for ( const u of incident . updates ) {
185+ ux . log ( `${ color . yellow ( u . update_type . padEnd ( padding ) ) } ${ new Date ( u . updated_at ) . toISOString ( ) } (${ formatDistanceToNow ( new Date ( u . updated_at ) ) } ago)` )
186+ ux . log ( `${ u . contents } \n` )
187+ }
188+ }
189+ } else if ( formattedTrustStatus ) {
190+ for ( const incident of formattedTrustStatus . incidents ) {
191+ ux . log ( )
192+ hux . styledHeader ( `${ incident . id } ${ color . yellow ( incident . createdAt ) } ${ color . cyan ( `https://status.salesforce.com/incidents/${ incident . id } ` ) } ` )
49193
50- const padding = maxBy ( incident . updates , ( i : any ) => i . update_type . length ) . update_type . length + 0
51- for ( const u of incident . updates ) {
52- ux . log ( `${ color . yellow ( u . update_type . padEnd ( padding ) ) } ${ new Date ( u . updated_at ) . toISOString ( ) } (${ formatDistanceToNow ( new Date ( u . updated_at ) ) } ago)` )
53- ux . log ( `${ u . contents } \n` )
194+ const padding = getMaxUpdateTypeLength ( incident . IncidentEvents . map ( event => event . localizedType ?? event . type ) )
195+ for ( const event of incident . IncidentEvents ) {
196+ const eventType = event . localizedType ?? event . type
197+ ux . log ( `${ color . yellow ( eventType . padEnd ( padding ) ) } ${ new Date ( event . createdAt ) . toISOString ( ) } (${ formatDistanceToNow ( new Date ( event . createdAt ) ) } ago)` )
198+ ux . log ( `${ event . message } \n` )
199+ }
54200 }
55201 }
56202 }
0 commit comments