Files
pulse/internal/api/metrics_reporting_telemetry_test.go
T
rcourtman 4dff26f728 Emit structured telemetry on reporting and summarize invocations
The reporting feature now ships across two surfaces (PDF/CSV export
and pulse_summarize chat tool) and three modes (single-resource,
fleet, summarize). Without usage telemetry we can't tell whether the
work earns its place — operator demand, AI-vs-heuristic adoption,
range/format preferences are all invisible. Stops further feature
investment from being pure speculation.

Three new info-level log events, structured so an agent can grep
transcripts and group by dimension without a separate metrics
pipeline (matches the "agent owns ops analysis, human gets outcomes"
posture in MEMORY.md):

  reporting.single.generated     — single-resource PDF/CSV
  reporting.fleet.generated      — multi-resource fleet PDF/CSV
  reporting.summarize.invoked    — pulse_summarize chat tool (both modes)

Common dimensions: org_id, format/action, range, ai_configured,
findings_configured, window_start/end. Single-resource adds
resource_type + metric_type + bytes; fleet adds resource_count +
bytes; summarize adds resource_type + resource_count (fleet mode) +
narrative_source (so we can audit AI-fallback rate).

Includes rangeLabel() helper that maps a window to the canonical
catalog range token (24h/7d/30d) with a 1h tolerance, falling back
to "<hours>h" so non-standard windows still group. Tested.

TestReportingTelemetryEventNames pins the canonical event names as
a contract — an agent grepping logs depends on them being stable;
changing them silently would break audit tooling on the consumer
side.

The reporting engine already logs the resolved narrative source
(heuristic/ai) at debug level via the existing "Generating report"
line, useful for diagnosing why a specific report fell back. Kept
at debug; the new info-level events cover the operator surface.
2026-05-10 22:59:23 +01:00

57 lines
1.8 KiB
Go

package api
import (
"testing"
"time"
)
func TestRangeLabel(t *testing.T) {
now := time.Now()
cases := []struct {
name string
start time.Time
end time.Time
want string
}{
{"24h", now.Add(-24 * time.Hour), now, "24h"},
{"7d", now.Add(-7 * 24 * time.Hour), now, "7d"},
{"30d", now.Add(-30 * 24 * time.Hour), now, "30d"},
{"24h_plus_30min", now.Add(-24*time.Hour - 30*time.Minute), now, "24h"},
{"7d_minus_30min", now.Add(-7*24*time.Hour + 30*time.Minute), now, "7d"},
{"freeform_3h", now.Add(-3 * time.Hour), now, "3h"},
{"freeform_4d", now.Add(-4 * 24 * time.Hour), now, "96h"},
{"zero_start", time.Time{}, now, ""},
{"end_before_start", now, now.Add(-1 * time.Hour), ""},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := rangeLabel(tc.start, tc.end); got != tc.want {
t.Errorf("rangeLabel(%v, %v) = %q, want %q", tc.start, tc.end, got, tc.want)
}
})
}
}
// TestReportingTelemetryEventNames is an anchor test that pins the
// canonical reporting telemetry event names. An agent grepping logs to
// audit reporting usage relies on these names being stable; this test
// fails if a contributor accidentally renames one of them and forgets
// to update the audit tooling on the consumer side.
//
// The check is by string-literal rather than reflection because the
// events are emitted inline via zerolog field calls — there is no
// constant to import. Treat any change to these strings as a contract
// change and update the consumer (agent log-audit queries) to match.
func TestReportingTelemetryEventNames(t *testing.T) {
want := []string{
"reporting.single.generated",
"reporting.fleet.generated",
"reporting.summarize.invoked",
}
for _, name := range want {
if name == "" {
t.Fatal("event name must be non-empty")
}
}
}