React - Observability & Logging
Overview
Estimated time: 25–35 minutes
Learn lightweight patterns for logging user actions, measuring timing, and reporting errors without external tooling.
Try it: Structured logger with user action tracing
View source
function createLogger(send){
let seq = 0;
return {
info(event, data){ send({lvl:'info', ts:Date.now(), seq:++seq, event, ...data}); },
error(event, data){ send({lvl:'error', ts:Date.now(), seq:++seq, event, ...data}); }
};
}
function App(){
const [logs, setLogs] = React.useState([]);
const logger = React.useMemo(() => createLogger(entry => setLogs(ls => [...ls, entry])), []);
const [start, setStart] = React.useState(null);
async function simulateFetch(){
const opId = Math.random().toString(36).slice(2);
logger.info('fetch:start', {opId, route:'#/products'});
const t0 = performance.now();
await new Promise(r => setTimeout(r, 120));
const t1 = performance.now();
logger.info('fetch:success', {opId, ms: Math.round(t1 - t0), items: 3});
}
function handleAction(){
const actionId = Math.random().toString(36).slice(2);
logger.info('ui:click', {actionId, target:'primary-button'});
}
function startTimer(){ setStart(performance.now()); logger.info('timer:start', {}); }
function stopTimer(){ if(start!=null){ const ms=Math.round(performance.now()-start); logger.info('timer:stop',{ms}); setStart(null);} }
function crash(){ try { throw new Error('Boom'); } catch (e){ logger.error('error:handled', {message:e.message, stack:e.stack?.split('\n')[0]}); } }
return (
<div>
<div style={{display:'flex', gap:8, flexWrap:'wrap'}}>
<button onClick={handleAction}>Click (trace UI)</button>
<button onClick={simulateFetch}>Simulate fetch</button>
<button onClick={startTimer} disabled={start!=null}>Start timer</button>
<button onClick={stopTimer} disabled={start==null}>Stop timer</button>
<button onClick={crash}>Handled error</button>
</div>
<ol style={{marginTop:12}}>
{logs.map((l,i)=>(
<li key={i}>[{new Date(l.ts).toLocaleTimeString()}] {l.lvl.toUpperCase()} {l.event} {JSON.stringify(l)}</li>
))}
</ol>
</div>
);
}
ReactDOM.createRoot(document.getElementById('try-obs')).render(<App />);
Syntax primer
- Structured logs are plain objects containing
lvl
,ts
,seq
,event
, and additional fields. - Wrap logging behind a small factory so you can swap transports (console, POST, buffer) later.
- Use
performance.now()
to capture durations in milliseconds with good resolution.
Vocabulary
- Structured log: a machine-parsable event with consistent fields.
- Trace ID: an identifier that ties related events together (e.g., actionId, opId).
- Transport: the destination for logs (console, network, storage).
Common pitfalls
- Logging too much in render; prefer logging on effects or event handlers.
- Missing IDs to correlate events—include stable IDs for actions/ops.
- Leaking PII in logs—only capture necessary non-sensitive data.
Exercises
- Add a network transport that batches 10 logs and "sends" them (append to a second list) every 2 seconds.
- Tag logs with the current route (hash) and user ID (hardcoded) and verify it appears in each entry.
- Transform errors into a uniform shape containing message, name, and first stack line.
Checks for Understanding
- Why prefer structured logs over free-form strings?
- What identifiers help correlate related events across a flow?
Show answers
- They’re machine-parsable, filterable, and easier to analyze.
- Trace IDs like actionId/opId or request IDs.