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

  1. Add a network transport that batches 10 logs and "sends" them (append to a second list) every 2 seconds.
  2. Tag logs with the current route (hash) and user ID (hardcoded) and verify it appears in each entry.
  3. Transform errors into a uniform shape containing message, name, and first stack line.

Checks for Understanding

  1. Why prefer structured logs over free-form strings?
  2. What identifiers help correlate related events across a flow?
Show answers
  1. They’re machine-parsable, filterable, and easier to analyze.
  2. Trace IDs like actionId/opId or request IDs.