Skip to content

Commit c66f2f2

Browse files
danielpaulusUbuntuclaude
authored
feat(cli): add TRACEROUTE Hops column to checks stats [SIM-278] (#1385)
* feat(reporters): per-type TRACEROUTE/GRPC/SSL result rendering (P4-CLI-RESULTS) Render failure-debug diagnostics for the three uptime monitor types across every CLI result surface, mirroring the 4.1 public check-results fields: - rest/check-results.ts: typed TracerouteCheckResult/GrpcCheckResult/ SslCheckResult interfaces + additive fields on CheckResult/CheckResultField. - formatters/check-result-detail.ts: per-type terminal + markdown diagnostic block (checks results get) keyed on the typed fields. - reporters/util.ts: GRPC/SSL/TRACEROUTE branches in formatCheckResult (checkly test terminal), sourced from the runner artifact (checkRunData). - formatters/batch-stats.ts: add the three types to TIMING_TYPES. - reporters/json.ts: emit a per-type diagnostics object in the JSON report for a failed run (checkly test --reporter json). Snapshot + assertion tests for each type across all three surfaces. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(constructs): add GrpcMonitor, SslMonitor, TracerouteMonitor IaC constructs Implement the missing CLI constructs so users can author and deploy gRPC, SSL and Traceroute uptime monitors via checkly/constructs, and so backend CLI export templates that import these classes compile. Constructs follow the DNS/TCP/ICMP monitor pattern (extend Monitor, register with Session, validate, synthesize): - GrpcMonitor / GrpcRequest / GrpcConfig / GrpcMetadata + GrpcAssertionBuilder (RESPONSE_TIME, GRPC_STATUS_CODE, GRPC_HEALTHCHECK_STATUS, GRPC_RESPONSE, GRPC_METADATA). checkType GRPC, top-level degraded/maxResponseTime (<=30000). - SslMonitor / SslRequest / SslConfig / SecurityBaseline + SslAssertionBuilder (cert expiry, chain, hostname, TLS version, cipher, key size, etc). checkType SSL; response-time limits live in sslConfig (degraded/maxResponseTimeMs). - TracerouteMonitor / TracerouteRequest + TracerouteAssertionBuilder (RESPONSE_TIME, HOP_COUNT, PACKET_LOSS). checkType TRACEROUTE, top-level degraded/maxResponseTime (<=30000); port dropped for ICMP probes. Request shapes mirror the public API (verified against checkly-go-sdk types and the p5-parity captured payloads). Wires exports in constructs/index.ts, import codegen in check-codegen.ts (GRPC/SSL/TRACEROUTE -> *MonitorCodegen), and adds the three types to constants.CheckTypes. Tests: construct synthesize/validation/grouping specs, codegen specs (incl. ICMP port-strip and assertion builders), and a regression spec asserting the backend-style export snippets compile against checkly/constructs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(deploy): skip code-bundle upload when nothing was bundled archive.store() (the 'Uploading Playwright tests' step) was called unconditionally on every deploy. For a project of only uptime monitors (GRPC/SSL/TRACEROUTE), the bundler registers no files, so this uploaded an empty Playwright bundle — an unnecessary code-bundle upload in production, and a hard failure in devenv where it 500s on the storage backend. The remote code bundle is consumed only by Playwright check suites (via bundler.marker -> playwright-check.ts); browser checks upload snapshots separately. Add Bundler.isEmpty and skip store() when no files were registered. Refs T65. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(cli): wire gRPC/SSL/Traceroute analytics paths + connection-error rendering Addresses review (Simo) new-monitor checklist gaps: - src/rest/analytics.ts: add checkTypeToPath (grpc-checks/ssl/traceroute) and defaultMetrics for the three types, matching the backend analytics routes + metric registries (gRPC total_*, SSL handshakeTimeMs_*/daysUntilExpiry_avg, TRACEROUTE finalHopLatencyAvg_*/totalHops_avg). - src/reporters/util.ts: add the Connection Error subsection to the GRPC/SSL/ TRACEROUTE result blocks, mirroring ICMP/DNS. * docs(cli): AI context references + examples for gRPC/SSL/Traceroute monitors Addresses review (Simo) new-monitor checklist Phases 5 & 7: - ai-context/references/configure-{grpc,ssl,traceroute}-monitors.md + REFERENCES and EXAMPLE_CONFIGS entries in context.ts (inline exampleConfig). - examples/advanced-project{,-js}/src/__checks__/uptime/{grpc,ssl,traceroute} TS + JS examples with group, assertions, and doc-link comments. * test(cli): e2e deploy fixtures + Create assertions for gRPC/SSL/Traceroute Addresses review (Simo) new-monitor checklist Phase 6: - e2e/__tests__/fixtures/deploy-project/{grpc,ssl,traceroute}.check.ts (activated:false) - deploy.spec.ts: assert the three logical IDs in the Create output. * feat(cli): support TEXT_BODY assertions for gRPC monitors Mirror monorepo #2730 which added TEXT_BODY to grpcMonitorAssertionSources. Adds GrpcAssertionBuilder.textBody() (source TEXT_BODY) + codegen case, matching the API check's textBody() builder. * refactor(cli): restructure SslMonitor props per review (wire shape unchanged) Per @sorccu's review: make SslMonitor ergonomics consistent with other monitors. - degradedResponseTime/maxResponseTime -> construct top level (were in sslConfig) - hostname/port/ipFamily -> request top level (were in sslConfig) - sslClientCertificateId -> into sslConfig (was on request) synthesize() remaps to the identical API wire shape (request.sslConfig.* + request.sslClientCertificateId), so the payload the backend receives is unchanged. Updated codegen (wire->construct), unit + codegen + export-snippet tests, examples (ts+js), e2e fixture, and AI context. * refactor(cli): drop Ms suffix on SslConfig.handshakeTimeout per review @sorccu: we don't suffix timeout properties with Ms even when the value is milliseconds. Renames the construct field handshakeTimeoutMs -> handshakeTimeout; synthesize() and the codegen wire interface keep the API's handshakeTimeoutMs. * refactor(cli): emit SSL degraded/maxResponseTime at top level to match gRPC/traceroute wire * feat(cli): add TRACEROUTE Hops column to checks stats [SIM-278] Adds a bespoke `Hops` column for TRACEROUTE checks in `checkly checks stats`, mirroring the ICMP Latency/Pkt Loss bespoke-column pattern. TRACEROUTE keeps its final-hop-latency Resp columns (stays in TIMING_TYPES) and additionally shows hop count from the new hopCount_avg field. Follow-up to #1362 (stacked on feat/cli-pertype-result-rendering). hopCount_avg is populated by the SIM-278 backend change; until it deploys the column renders a dash. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Ubuntu <ubuntu@ip-172-31-7-108.eu-central-1.compute.internal> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ccecd24 commit c66f2f2

4 files changed

Lines changed: 110 additions & 0 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ export default class ChecksStats extends AuthCommand {
144144
responseTime_p95: r.analytics?.responseTime_p95 ?? null,
145145
latency_avg: r.analytics?.latency_avg ?? null,
146146
packetLoss_avg: r.analytics?.packetLoss_avg ?? null,
147+
hopCount_avg: r.analytics?.hopCount_avg ?? null,
147148
})),
148149
pagination: { page, limit, total: totalChecks, totalPages },
149150
range,
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { describe, it, expect } from 'vitest'
2+
import { formatBatchStats, type StatsRow } from '../batch-stats.js'
3+
4+
// Build a minimal StatsRow. Only the fields read by the batch-stats formatter
5+
// (name, checkType, activated, status, analytics) matter here.
6+
function row (
7+
checkType: string,
8+
analytics: Partial<StatsRow['analytics']> = {},
9+
name = `${checkType} check`,
10+
): StatsRow {
11+
return {
12+
name,
13+
checkType,
14+
activated: true,
15+
status: { hasFailures: false, hasErrors: false, isDegraded: false },
16+
analytics: { availability: 99.9, ...analytics },
17+
} as unknown as StatsRow
18+
}
19+
20+
describe('formatBatchStats — TRACEROUTE columns', () => {
21+
it('renders a Hops column with the average hop count for TRACEROUTE rows', () => {
22+
const out = formatBatchStats([row('TRACEROUTE', { hopCount_avg: 12.5 })], 'last24Hours', 'md')
23+
24+
expect(out).toContain('Hops (avg)')
25+
expect(out).toContain('12.50')
26+
})
27+
28+
it('keeps the final-hop-latency responseTime columns for TRACEROUTE (no regression)', () => {
29+
// responseTime_avg for TRACEROUTE is the final-hop latency (ms).
30+
const out = formatBatchStats(
31+
[row('TRACEROUTE', { hopCount_avg: 9, responseTime_avg: 42000 })],
32+
'last24Hours',
33+
'md',
34+
)
35+
36+
expect(out).toContain('Resp (avg)')
37+
expect(out).toContain('42.000s')
38+
expect(out).toContain('Hops (avg)')
39+
})
40+
41+
it('shows a dash in the Hops column for non-TRACEROUTE rows in a mixed batch', () => {
42+
const out = formatBatchStats(
43+
[
44+
row('API', { responseTime_avg: 1000 }),
45+
row('TRACEROUTE', { hopCount_avg: 15 }),
46+
],
47+
'last24Hours',
48+
'md',
49+
)
50+
51+
expect(out).toContain('Hops (avg)')
52+
// The API row has no hop count; the Hops cell falls back to the dash.
53+
expect(out).toContain('—')
54+
expect(out).toContain('15.00')
55+
})
56+
57+
it('omits the Hops column entirely when no TRACEROUTE checks are present', () => {
58+
const out = formatBatchStats([row('API', { responseTime_avg: 1000 })], 'last24Hours', 'md')
59+
60+
expect(out).not.toContain('Hops')
61+
})
62+
63+
it('renders a dash when a TRACEROUTE row has no hop-count data yet', () => {
64+
// Until the backend batch-analytics endpoint returns hopCount_avg, the column
65+
// renders a dash rather than a bogus value.
66+
const out = formatBatchStats([row('TRACEROUTE', { hopCount_avg: null })], 'last24Hours', 'md')
67+
68+
expect(out).toContain('Hops (avg)')
69+
expect(out).toContain('—')
70+
})
71+
72+
it('renders the Hops column in terminal format (count for traceroute, dash otherwise)', () => {
73+
const out = formatBatchStats(
74+
[
75+
row('API', { responseTime_avg: 1000 }),
76+
row('TRACEROUTE', { hopCount_avg: 15 }),
77+
],
78+
'last24Hours',
79+
'terminal',
80+
)
81+
82+
// Terminal renders headers upper-cased ("HOPS"); the value and the
83+
// non-traceroute dash both survive chalk styling.
84+
expect(out).toContain('HOPS')
85+
expect(out).toContain('15.00')
86+
expect(out).toContain('—')
87+
})
88+
})

packages/cli/src/formatters/batch-stats.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ import type { QuickRange } from '../rest/analytics.js'
88

99
export type StatsRow = CheckWithStatus & { analytics?: BatchAnalyticsResult }
1010

11+
// TRACEROUTE stays in TIMING_TYPES so it keeps its final-hop-latency responseTime
12+
// columns; it additionally gets a bespoke Hops column (see hasTraceroute below).
1113
const TIMING_TYPES = new Set(['API', 'BROWSER', 'PLAYWRIGHT', 'MULTI_STEP', 'URL', 'TCP', 'DNS', 'GRPC', 'SSL', 'TRACEROUTE'])
1214
const ICMP_TYPE = 'ICMP'
15+
const TRACEROUTE_TYPE = 'TRACEROUTE'
1316
const DASH = '—'
1417

1518
/**
@@ -63,6 +66,7 @@ function metricOrDash (
6366
function buildColumns (rows: StatsRow[], format: OutputFormat): ColumnDef<StatsRow>[] {
6467
const hasTiming = rows.some(r => TIMING_TYPES.has(r.checkType))
6568
const hasIcmp = rows.some(r => r.checkType === ICMP_TYPE)
69+
const hasTraceroute = rows.some(r => r.checkType === TRACEROUTE_TYPE)
6670

6771
if (format === 'md') {
6872
const cols: ColumnDef<StatsRow>[] = [
@@ -99,6 +103,13 @@ function buildColumns (rows: StatsRow[], format: OutputFormat): ColumnDef<StatsR
99103
value: (r, fmt) => metricOrDash('packetLoss_avg', r.analytics?.packetLoss_avg ?? null, fmt, r.checkType === ICMP_TYPE),
100104
})
101105
}
106+
if (hasTraceroute) {
107+
cols.push({
108+
header: 'Hops (avg)',
109+
align: 'right',
110+
value: (r, fmt) => metricOrDash('hopCount_avg', r.analytics?.hopCount_avg ?? null, fmt, r.checkType === TRACEROUTE_TYPE),
111+
})
112+
}
102113
return cols
103114
}
104115

@@ -162,6 +173,15 @@ function buildColumns (rows: StatsRow[], format: OutputFormat): ColumnDef<StatsR
162173
})
163174
}
164175

176+
if (hasTraceroute) {
177+
cols.push({
178+
header: 'Hops',
179+
width: metricWidth,
180+
align: 'right',
181+
value: (r, fmt) => metricOrDash('hopCount_avg', r.analytics?.hopCount_avg ?? null, fmt, r.checkType === TRACEROUTE_TYPE),
182+
})
183+
}
184+
165185
return cols
166186
}
167187

packages/cli/src/rest/batch-analytics.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface BatchAnalyticsResult {
1515
packetLoss_avg: number | null
1616
packetLoss_p95: number | null
1717
packetLoss_p99: number | null
18+
hopCount_avg: number | null
1819
}
1920

2021
// Subset of QuickRange — excludes last30Days and thisMonth because their rolling windows

0 commit comments

Comments
 (0)