Welcome to the final chapter! In the previous chapter, Notification Pipeline, we learned how to talk to the user.
Now we need to listen to the application itself.
Imagine driving a car with a blacked-out dashboard. You are moving, but you don't know your speed, how much gas is left, or if the engine is overheating.
In software, this happens when you release an app.
console.log is not enough. It scrolls away and doesn't do math (like calculating averages).
This chapter introduces the Telemetry & Stats Store. It acts as your application's dashboard, collecting metrics like counters, gauges, and timers to help you diagnose performance and usage.
Our Stats Store provides three main tools, similar to a car's dashboard:
Let's say we want to count how many times a user presses a "Copy" button. We use useCounter.
import { useCounter } from './stats';
function CopyButton() {
// 1. Get a counter function tied to a specific metric name
const countCopy = useCounter('copy_actions_total');
return (
<Button onPress={() => {
// 2. Increment the counter by 1
countCopy(1);
copyTextToClipboard();
}}>
Copy Text
</Button>
);
}
What happens?
Somewhere in the background, copy_actions_total goes from 0 -> 1. At the end of the session, you'll see exactly how many times this feature was used.
Timers are more complex. If an operation takes 100ms once, but 5 seconds the next time, an "Average" might mislead you. We need to know the distribution.
Our useTimer acts as a Histogram. It records values and automatically calculates the "P99" (the 99th percentileβmeaning "99% of requests were faster than this").
import { useTimer } from './stats';
function AIResponder() {
// 1. Create the timer tracker
const recordLatency = useTimer('ai_response_time_ms');
const generate = async () => {
const start = Date.now();
await callAI(); // The slow operation
const duration = Date.now() - start;
// 2. Record the duration
recordLatency(duration);
};
// ...
}
Result: When the app closes, the logs will show:
ai_response_time_ms_avg: 450msai_response_time_ms_p99: 1200ms (This tells you that occasionally, it is quite slow!)In a Terminal UI, if your logic is too heavy, the interface feels "laggy." We call this a drop in FPS (Frames Per Second).
The fpsMetrics system is a specialized sensor that watches how often the screen repaints.
This is usually displayed in a debug footer.
import { useFpsMetrics } from './fpsMetrics';
function DebugFooter() {
// 1. Get the metrics getter
const getMetrics = useFpsMetrics();
// 2. Read the current value
const fps = getMetrics?.()?.fps ?? 60;
return <Text color="gray">FPS: {fps}</Text>;
}
Why is this separate? FPS updates constantly (every few milliseconds). We keep it separate from the main Stats Store to avoid overhead.
How does the data get saved? The Stats Store is an In-Memory Database that lives as long as the app runs. When the app exits, it quickly flushes data to a file.
Open stats.tsx. This file contains the logic for aggregating numbers.
The store is just a collection of JavaScript Maps.
// stats.tsx (Simplified)
export function createStatsStore() {
// Simple counters
const metrics = new Map<string, number>();
// Complex timers
const histograms = new Map<string, Histogram>();
return {
increment(name, value = 1) {
// Get current value, add new value, save it.
const current = metrics.get(name) ?? 0;
metrics.set(name, current + value);
},
// ... other methods
};
}
When you record a time, we don't just save one number. We add it to a "Reservoir" (a sample list) so we can calculate percentiles later.
// stats.tsx (Simplified)
observe(name, value) {
let h = histograms.get(name);
if (!h) {
// Initialize if new
h = { reservoir: [], count: 0, sum: 0, min: value, max: value };
histograms.set(name, h);
}
h.count++;
h.sum += value;
// Add to the list of samples
h.reservoir.push(value);
}
The most important part of telemetry is ensuring data isn't lost when the user quits. We use process.on('exit') inside the Provider.
// stats.tsx (Simplified)
useEffect(() => {
const flush = () => {
const data = store.getAll();
// Save to disk function
saveCurrentProjectConfig(cfg => ({
...cfg,
lastSessionMetrics: data
}));
};
// Listen for the "death" of the process
process.on('exit', flush);
return () => process.off('exit', flush);
}, [store]);
This ensures that even if you press Ctrl+C, the application takes a millisecond to dump its stats to your config file before closing.
In this final chapter, we built the "Black Box" recorder for our application:
useCounter and useTimer to track what users do and how long it takes.StatsStore mathematically combines them into useful summaries (averages, totals).Congratulations! You have completed the context project tutorial. You now understand the core architecture of a complex Terminal User Interface:
You are now ready to build powerful, performant, and robust terminal applications. Happy coding!
Generated by Code IQ