Skip to content

Commit 3e87983

Browse files
committed
wip
1 parent 0897ec7 commit 3e87983

File tree

1 file changed

+156
-120
lines changed

1 file changed

+156
-120
lines changed

src/commands/analytics.ts

Lines changed: 156 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,19 @@ import contrib from 'blessed-contrib'
44
import meow from 'meow'
55
import ora from 'ora'
66

7-
import { outputFlags, validationFlags } from '../flags'
7+
import { outputFlags } from '../flags'
88
import { handleApiCall, handleUnsuccessfulApiResponse } from '../utils/api-helpers'
99
import { AuthError, InputError } from '../utils/errors'
1010
import { printFlagList } from '../utils/formatting'
1111
import { getDefaultKey, setupSdk } from '../utils/sdk'
1212

1313
import type { CliSubcommand } from '../utils/meow-with-subcommands'
1414
import type { Ora } from "ora"
15+
import chalk from 'chalk'
1516

1617
export const analytics: CliSubcommand = {
17-
description: 'Look up analytics data',
18+
description: `Look up analytics data \n
19+
Default parameters are set to show the organization-level analytics over the last 7 days.`,
1820
async run (argv, importMeta, { parentName }) {
1921
const name = parentName + ' analytics'
2022

@@ -36,31 +38,53 @@ export const analytics: CliSubcommand = {
3638
}
3739
}
3840

41+
const analyticsFlags: { [key: string]: any } = {
42+
scope: {
43+
type: 'string',
44+
shortFlag: 's',
45+
default: 'org',
46+
description: "Scope of the analytics data - either 'org' or 'repo'"
47+
},
48+
time: {
49+
type: 'number',
50+
shortFlag: 't',
51+
default: 7,
52+
description: 'Time filter - either 7, 30 or 90'
53+
},
54+
repo: {
55+
type: 'string',
56+
shortFlag: 'r',
57+
default: '',
58+
description: "Name of the repository"
59+
},
60+
}
61+
3962
// Internal functions
4063

4164
type CommandContext = {
4265
scope: string
43-
time: string
44-
repo: string | undefined
66+
time: number
67+
repo: string
4568
outputJson: boolean
4669
}
4770

4871
function setupCommand (name: string, description: string, argv: readonly string[], importMeta: ImportMeta): void|CommandContext {
4972
const flags: { [key: string]: any } = {
5073
...outputFlags,
51-
...validationFlags,
74+
...analyticsFlags
5275
}
5376

5477
const cli = meow(`
5578
Usage
56-
$ ${name} <scope> <time>
79+
$ ${name} --scope=<scope> --time=<time filter>
5780
5881
Options
5982
${printFlagList(flags, 6)}
6083
6184
Examples
62-
$ ${name} org 7
63-
$ ${name} org 30
85+
$ ${name} --scope=org --time=7
86+
$ ${name} --scope=org --time=30
87+
$ ${name} --scope=repo --repo=test-repo --time=30
6488
`, {
6589
argv,
6690
description,
@@ -69,117 +93,140 @@ function setupCommand (name: string, description: string, argv: readonly string[
6993
})
7094

7195
const {
72-
json: outputJson
96+
json: outputJson,
97+
scope,
98+
time,
99+
repo
73100
} = cli.flags
74101

75-
const scope = cli.input[0]
76-
77-
if (!scope) {
78-
throw new InputError('Please provide a scope to get analytics data')
102+
if (scope !== 'org' && scope !== 'repo') {
103+
throw new InputError("The scope must either be 'org' or 'repo'")
79104
}
80105

81-
if (!cli.input.length) {
82-
throw new InputError('Please provide a scope and a time to get analytics data')
106+
if (time !== 7 && time !== 30 && time !== 90) {
107+
throw new InputError('The time filter must either be 7, 30 or 90')
83108
}
84109

85-
if (scope && !['org', 'repo'].includes(scope)) {
86-
throw new InputError("The scope must either be 'scope' or 'repo'")
110+
if(scope === 'repo' && !repo){
111+
console.error(
112+
`${chalk.bgRed.white('Input error')}: Please provide a repository name when using the repository scope. \n`
113+
)
114+
cli.showHelp()
115+
return
87116
}
88117

89-
const repo = scope === 'repo' ? cli.input[1] : undefined
118+
return <CommandContext>{
119+
scope, time, repo, outputJson
120+
}
121+
}
90122

91-
const time = scope === 'repo' ? cli.input[2] : cli.input[1]
123+
async function fetchOrgAnalyticsData (time: number, spinner: Ora, apiKey: string, outputJson: boolean): Promise<void> {
124+
const socketSdk = await setupSdk(apiKey)
125+
const result = await handleApiCall(socketSdk.getOrgAnalytics(time.toString()), 'fetching analytics data')
92126

93-
if (!time) {
94-
throw new InputError('Please provide a time to get analytics data')
127+
if (result.success === false) {
128+
return handleUnsuccessfulApiResponse('getOrgAnalytics', result, spinner)
95129
}
96130

97-
if (time && !['7', '30', '60'].includes(time)) {
98-
throw new InputError('The time filter must either be 7, 30 or 60')
131+
spinner.stop()
132+
133+
if(!result.data.length){
134+
return console.log('No analytics data is available for this organization yet.')
99135
}
100136

101-
return <CommandContext>{
102-
scope, time, repo, outputJson
137+
const data = formatData(result.data)
138+
139+
if(outputJson){
140+
return console.log(data)
103141
}
142+
143+
return displayAnalyticsScreen(data)
104144
}
105145

106-
async function fetchOrgAnalyticsData (time: string, spinner: Ora, apiKey: string, outputJson: boolean): Promise<void> {
146+
async function fetchRepoAnalyticsData (repo: string, time: number, spinner: Ora, apiKey: string, outputJson: boolean): Promise<void> {
107147
const socketSdk = await setupSdk(apiKey)
108-
const result = await handleApiCall(socketSdk.getOrgAnalytics(time), 'fetching analytics data')
148+
const result = await handleApiCall(socketSdk.getRepoAnalytics(repo, time.toString()), 'fetching analytics data')
109149

110150
if (result.success === false) {
111-
return handleUnsuccessfulApiResponse('getOrgAnalytics', result, spinner)
151+
return handleUnsuccessfulApiResponse('getRepoAnalytics', result, spinner)
112152
}
113-
114153
spinner.stop()
115154

116-
const data = result.data.reduce((acc: { [key: string]: any }, current) => {
117-
const formattedDate = new Date(current.created_at).toLocaleDateString()
118-
119-
if (acc[formattedDate]) {
120-
acc[formattedDate].total_critical_alerts += current.total_critical_alerts
121-
acc[formattedDate].total_high_alerts += current.total_high_alerts
122-
acc[formattedDate].total_critical_added += current.total_critical_added
123-
acc[formattedDate].total_high_added += current.total_high_added
124-
acc[formattedDate].total_critical_prevented += current.total_critical_prevented
125-
acc[formattedDate].total_high_prevented += current.total_high_prevented
126-
acc[formattedDate].total_medium_prevented += current.total_medium_prevented
127-
acc[formattedDate].total_low_prevented += current.total_low_prevented
128-
} else {
129-
acc[formattedDate] = current
130-
acc[formattedDate].created_at = formattedDate
131-
}
155+
if(!result.data.length){
156+
return console.log('No analytics data is available for this organization yet.')
157+
}
132158

133-
return acc
134-
}, {})
159+
const data = formatData(result.data)
135160

136161
if(outputJson){
137162
return console.log(data)
138163
}
139164

140-
const screen = blessed.screen()
141-
// eslint-disable-next-line
142-
const grid = new contrib.grid({rows: 4, cols: 4, screen})
165+
return displayAnalyticsScreen(data)
166+
}
143167

144-
renderLineCharts(grid, screen, 'Total critical alerts', [0,0,1,2], data, 'total_critical_alerts')
145-
renderLineCharts(grid, screen, 'Total high alerts', [0,2,1,2], data, 'total_high_alerts')
146-
renderLineCharts(grid, screen, 'Total critical alerts added to main', [1,0,1,2], data, 'total_critical_added')
147-
renderLineCharts(grid, screen, 'Total high alerts added to main', [1,2,1,2], data, 'total_high_added')
148-
renderLineCharts(grid, screen, 'Total critical alerts prevented from main', [2,0,1,2], data, 'total_critical_prevented')
149-
renderLineCharts(grid, screen, 'Total high alerts prevented from main', [2,2,1,2], data, 'total_high_prevented')
168+
const renderLineCharts = (grid: any, screen: any, title: string, coords: number[], data: FormattedAnalyticsData, label: string) => {
169+
const formattedDates = Object.keys(data).map(d => `${new Date(d).getMonth()+1}/${new Date(d).getDate()}`)
150170

151-
const bar = grid.set(3, 0, 1, 2, contrib.bar,
152-
{ label: 'Top 5 alert types'
153-
, barWidth: 10
154-
, barSpacing: 17
155-
, xOffset: 0
156-
, maxHeight: 9, barBgColor: 'magenta' })
171+
// @ts-ignore
172+
const alertsCounts = Object.values(data).map(d => d[label])
173+
174+
const line = grid.set(...coords, contrib.line,
175+
{ style:
176+
{ line: "cyan",
177+
text: "cyan",
178+
baseline: "black"
179+
},
180+
xLabelPadding: 0,
181+
xPadding: 0,
182+
xOffset: 0,
183+
wholeNumbersOnly: true,
184+
legend: {
185+
width: 1
186+
},
187+
label: title
188+
}
189+
)
157190

158-
screen.append(bar) //must append before setting data
191+
screen.append(line)
159192

160-
const top5AlertTypes = Object.values(data)[0].top_five_alert_types
161-
162-
bar.setData(
163-
{ titles: Object.keys(top5AlertTypes)
164-
, data: Object.values(top5AlertTypes)})
193+
const lineData = {
194+
x: formattedDates.reverse(),
195+
y: alertsCounts
196+
}
165197

166-
screen.render()
167-
168-
screen.key(['escape', 'q', 'C-c'], function() {
169-
return process.exit(0);
170-
})
198+
line.setData([lineData])
171199
}
172200

173-
async function fetchRepoAnalyticsData (repo: string, time: string, spinner: Ora, apiKey: string, outputJson: boolean): Promise<void> {
174-
const socketSdk = await setupSdk(apiKey)
175-
const result = await handleApiCall(socketSdk.getRepoAnalytics(repo, time), 'fetching analytics data')
176-
177-
if (result.success === false) {
178-
return handleUnsuccessfulApiResponse('getRepoAnalytics', result, spinner)
201+
type AnalyticsData = {
202+
id: number,
203+
created_at: string
204+
repository_id: string
205+
organization_id: number
206+
repository_name: string
207+
total_critical_alerts: number
208+
total_high_alerts: number
209+
total_medium_alerts: number
210+
total_low_alerts: number
211+
total_critical_added: number
212+
total_high_added: number
213+
total_medium_added: number
214+
total_low_added: number
215+
total_critical_prevented: number
216+
total_high_prevented: number
217+
total_medium_prevented: number
218+
total_low_prevented: number
219+
top_five_alert_types: {
220+
[key: string]: number
179221
}
180-
spinner.stop()
222+
}
223+
224+
type FormattedAnalyticsData = {
225+
[key: string]: AnalyticsData
226+
}
181227

182-
const data = result.data.reduce((acc: { [key: string]: any }, current) => {
228+
const formatData = (data: AnalyticsData[]) => {
229+
return data.reduce((acc: { [key: string]: any }, current) => {
183230
const formattedDate = new Date(current.created_at).toLocaleDateString()
184231

185232
if (acc[formattedDate]) {
@@ -198,11 +245,9 @@ async function fetchRepoAnalyticsData (repo: string, time: string, spinner: Ora,
198245

199246
return acc
200247
}, {})
248+
}
201249

202-
if(outputJson){
203-
return console.log(data)
204-
}
205-
250+
const displayAnalyticsScreen = (data: FormattedAnalyticsData) => {
206251
const screen = blessed.screen()
207252
// eslint-disable-next-line
208253
const grid = new contrib.grid({rows: 4, cols: 4, screen})
@@ -222,48 +267,39 @@ async function fetchRepoAnalyticsData (repo: string, time: string, spinner: Ora,
222267
, maxHeight: 9, barBgColor: 'magenta' })
223268

224269
screen.append(bar) //must append before setting data
225-
226-
const top5AlertTypes = Object.values(data)[0].top_five_alert_types
270+
271+
const top5 = extractTop5Alerts(data)
227272

228273
bar.setData(
229-
{ titles: Object.keys(top5AlertTypes)
230-
, data: Object.values(top5AlertTypes)})
274+
{ titles: Object.keys(top5)
275+
, data: Object.values(top5)})
231276

232277
screen.render()
233278

234-
screen.key(['escape', 'q', 'C-c'], function() {
235-
return process.exit(0);
236-
})
279+
screen.key(['escape', 'q', 'C-c'], () => process.exit(0))
237280
}
238281

239-
const renderLineCharts = (grid: any, screen: any, title: string, coords: number[], data: {[key: string]: {[key: string]: number}}, label: string) => {
240-
const formattedDates = Object.keys(data).map(d => `${new Date(d).getMonth()+1}/${new Date(d).getDate()}`)
241-
242-
const alertsCounts = Object.values(data).map(d => d[label])
282+
const extractTop5Alerts = (data: FormattedAnalyticsData) => {
283+
const allTop5Alerts = Object.values(data).map(d => d.top_five_alert_types)
243284

244-
const line = grid.set(...coords, contrib.line,
245-
{ style:
246-
{ line: "cyan",
247-
text: "cyan",
248-
baseline: "black"
249-
},
250-
xLabelPadding: 0,
251-
xPadding: 0,
252-
xOffset: 0,
253-
wholeNumbersOnly: true,
254-
legend: {
255-
width: 1
256-
},
257-
label: title
258-
}
259-
)
260-
261-
screen.append(line)
262-
263-
const lineData = {
264-
x: formattedDates.reverse(),
265-
y: alertsCounts
266-
}
285+
const aggTop5Alerts = allTop5Alerts.reduce((acc, current) => {
286+
const alertTypes = Object.keys(current)
287+
288+
alertTypes.forEach(type => {
289+
if(!acc[type]){
290+
// @ts-ignore
291+
acc[type] = current[type]
292+
} else {
293+
// @ts-ignore
294+
if(acc[type] < current[type]){
295+
// @ts-ignore
296+
acc[type] = current[type]
297+
}
298+
}
299+
})
300+
301+
return acc
302+
}, {})
267303

268-
line.setData([lineData])
304+
return Object.fromEntries(Object.entries(aggTop5Alerts).sort((a: [string, number], b: [string, number]) => b[1] - a[1]).slice(0,5))
269305
}

0 commit comments

Comments
 (0)