Skip to content

Commit 3a80e09

Browse files
feat(cli): show analytics stats in checks get [AI-55] (#1249)
* refactor(cli): extract analytics formatting into shared module * feat(cli): show analytics stats in checks get output [AI-55] * fix(cli): tighten extractMetrics return type and stats header style - Return Record<string, number> with isNaN guard instead of Record<string, number | string> - Use dim styling for range in STATS header for visual consistency * chore(cli): remove standalone checks stats command Stats are shown inline in checks get, no separate subcommand needed. * chore(cli): wire analytics REST client into API module * refactor(cli): use shared CheckTypes constant for check type mappings Expands CheckTypes in constants.ts to include all 9 check types and uses it in analytics.ts and list.ts instead of hardcoded strings. * fix(cli): handle array series data and clean up stats output - Fix extractMetrics to handle API returning series[0].data as array - Filter out bonus fields (total, success) by using requestedMetrics - Display metrics in intended order (availability first) * fix(cli): use human-friendly range labels in stats output "last24Hours" → "last 24 hours", "last7Days" → "last 7 days", etc. * feat(cli): add --group-by, --metrics, --filter-status flags and fix error rendering - Add --group-by location|statusCode for per-region/status-code stats breakdown - Add --metrics flag for custom metric selection (overrides defaults) - Add --filter-status success|failure to filter stats by run outcome - Update browser/playwright defaults to include Web Vitals (LCP, CLS, TBT) - Add CLS as "score" unit type with appropriate thresholds - Fix formatMetricLabel to preserve uppercase acronyms (LCP, CLS, TBT) - Fix Analytics.get() return type to match { data: T } pattern - Replace non-null assertions with ?? fallbacks - Remove dead listMetrics() method - Use API metadata for unit detection with heuristic fallback - Fix [object Object] rendering for Playwright Test error objects - Expand test coverage: findUnit branches, grouped output, metadata, error objects --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4301be2 commit 3a80e09

10 files changed

Lines changed: 752 additions & 8 deletions

File tree

packages/cli/src/commands/checks/get.ts

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
formatErrorGroups,
1212
} from '../../formatters/checks'
1313
import { formatResultDetail } from '../../formatters/check-result-detail'
14+
import { quickRangeValues, type QuickRange, type GroupBy } from '../../rest/analytics'
15+
import { formatAnalyticsSection } from '../../formatters/analytics'
1416

1517
export default class ChecksGet extends AuthCommand {
1618
static hidden = false
@@ -41,6 +43,22 @@ export default class ChecksGet extends AuthCommand {
4143
'results-cursor': Flags.string({
4244
description: 'Cursor for results pagination (from previous output).',
4345
}),
46+
'stats-range': Flags.string({
47+
description: 'Time range for stats.',
48+
options: quickRangeValues,
49+
default: 'last24Hours',
50+
}),
51+
'group-by': Flags.string({
52+
description: 'Group stats by dimension.',
53+
options: ['location', 'statusCode'],
54+
}),
55+
'metrics': Flags.string({
56+
description: 'Comma-separated list of metrics to show (overrides defaults).',
57+
}),
58+
'filter-status': Flags.string({
59+
description: 'Only include runs with this status in stats.',
60+
options: ['success', 'failure'],
61+
}),
4462
'output': outputFlag({ default: 'detail' }),
4563
}
4664

@@ -59,22 +77,32 @@ export default class ChecksGet extends AuthCommand {
5977
return await this.showErrorGroupDetail(args.id, flags['error-group'], flags.output ?? 'detail')
6078
}
6179

62-
const [{ data: check }, statusResp, resultsResp, errorGroupsResp] = await Promise.all([
63-
api.checks.get(args.id),
80+
// Fetch check first (need checkType for analytics)
81+
const { data: check } = await api.checks.get(args.id)
82+
83+
// Fetch remaining data in parallel
84+
const [statusResp, resultsResp, errorGroupsResp, analyticsResp] = await Promise.all([
6485
api.checkStatuses.get(args.id).catch(() => ({ data: undefined })),
6586
api.checkResults.getAll(args.id, {
6687
limit: flags['results-limit'],
6788
nextId: flags['results-cursor'],
6889
}).catch(() => ({ data: { entries: [], nextId: null, length: 0 } })),
6990
api.errorGroups.getByCheckId(args.id).catch(() => ({ data: [] })),
91+
api.analytics.get(args.id, check.checkType, {
92+
quickRange: (flags['stats-range'] ?? 'last24Hours') as QuickRange,
93+
groupBy: flags['group-by'] === 'location' ? 'runLocation' : flags['group-by'] as GroupBy | undefined,
94+
metrics: flags.metrics ? flags.metrics.split(',').map(m => m.trim()) : undefined,
95+
filterByStatus: flags['filter-status'] as 'success' | 'failure' | undefined,
96+
}).then(r => r.data).catch(() => undefined),
7097
])
7198

7299
const status = statusResp.data
73100
const { entries: results, nextId } = resultsResp.data
74101
const errorGroups = errorGroupsResp.data
75102

76103
if (flags.output === 'json') {
77-
this.log(JSON.stringify({ check, status, results, nextId, errorGroups }, null, 2))
104+
const analytics = analyticsResp ?? null
105+
this.log(JSON.stringify({ check, status, results, nextId, errorGroups, analytics }, null, 2))
78106
return
79107
}
80108

@@ -85,6 +113,11 @@ export default class ChecksGet extends AuthCommand {
85113
const lines = [
86114
formatCheckDetail(checkWithStatus, fmt),
87115
]
116+
const statsOutput = formatAnalyticsSection(analyticsResp, flags['stats-range'] ?? 'last24Hours', fmt)
117+
if (statsOutput) {
118+
lines.push('')
119+
lines.push(statsOutput)
120+
}
88121
const errorGroupsOutput = formatErrorGroups(errorGroups, fmt)
89122
if (errorGroupsOutput) {
90123
lines.push('')
@@ -106,6 +139,12 @@ export default class ChecksGet extends AuthCommand {
106139
output.push(formatCheckDetail(checkWithStatus, fmt))
107140
output.push('')
108141

142+
const statsOutput = formatAnalyticsSection(analyticsResp, flags['stats-range'] ?? 'last24Hours', fmt)
143+
if (statsOutput) {
144+
output.push(statsOutput)
145+
output.push('')
146+
}
147+
109148
const errorGroupsOutput = formatErrorGroups(errorGroups, fmt)
110149
if (errorGroupsOutput) {
111150
output.push(errorGroupsOutput)
@@ -133,6 +172,8 @@ export default class ChecksGet extends AuthCommand {
133172
if (nextId) {
134173
output.push(` ${chalk.dim('More results:')} checkly checks get ${args.id} --results-cursor ${nextId}`)
135174
}
175+
output.push(` ${chalk.dim('Change range:')} checkly checks get ${args.id} --stats-range last7Days`)
176+
output.push(` ${chalk.dim('By region:')} checkly checks get ${args.id} --group-by location`)
136177
output.push(` ${chalk.dim('Back to list:')} checkly checks list`)
137178

138179
this.log(output.join('\n'))

packages/cli/src/commands/checks/list.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { outputFlag } from '../../helpers/flags'
55
import * as api from '../../rest/api'
66
import type { CheckWithStatus } from '../../formatters/checks'
77
import type { OutputFormat } from '../../formatters/render'
8+
import { allCheckTypes } from '../../constants'
89
import {
910
formatChecks,
1011
formatSummaryBar,
@@ -41,7 +42,7 @@ export default class ChecksList extends AuthCommand {
4142
}),
4243
'type': Flags.string({
4344
description: 'Filter by check type.',
44-
options: ['API', 'BROWSER', 'MULTI_STEP', 'HEARTBEAT', 'PLAYWRIGHT', 'TCP', 'DNS', 'ICMP', 'URL'],
45+
options: allCheckTypes,
4546
}),
4647
'status': Flags.string({
4748
description: 'Filter by check status.',

packages/cli/src/constants.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@ export const CheckTypes = {
33
BROWSER: 'BROWSER',
44
HEARTBEAT: 'HEARTBEAT',
55
MULTI_STEP: 'MULTI_STEP',
6-
}
6+
PLAYWRIGHT: 'PLAYWRIGHT',
7+
TCP: 'TCP',
8+
ICMP: 'ICMP',
9+
DNS: 'DNS',
10+
URL: 'URL',
11+
} as const
12+
13+
export type CheckType = typeof CheckTypes[keyof typeof CheckTypes]
14+
15+
export const allCheckTypes = Object.values(CheckTypes)
716

817
export const LOGICAL_ID_PATTERN = /^[A-Za-z0-9_\-/#.]+$/

packages/cli/src/formatters/__tests__/__fixtures__/fixtures.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,18 @@ export const apiCheckResultWithError: CheckResult = {
335335
} as ApiCheckResult,
336336
}
337337

338+
export const browserCheckResultWithObjectErrors: CheckResult = {
339+
...browserCheckResult,
340+
id: 'result-6',
341+
browserCheckResult: {
342+
...browserCheckResult.browserCheckResult!,
343+
// The API sometimes returns error objects instead of strings
344+
errors: [
345+
{ name: 'Error', message: 'expect(received).toBe(expected) // Object.is equality' },
346+
] as any,
347+
} as BrowserCheckResult,
348+
}
349+
338350
export const minimalCheckResult: CheckResult = {
339351
id: 'result-4',
340352
checkId: 'check-6',

0 commit comments

Comments
 (0)