1. The plain app
First the app, with no telemetry at all: an API with three routes and a page that calls them. When it works, the next pages add the stack and the instrumentation one piece at a time, so you always know which change did what.
Every command goes in its own block: run them one by one, in order. When a step says “replace the file”, select everything in it and paste the new content over it.
GET /api/products · POST /api/checkout · GET /api/fail
Step 0. Check your tools
Section titled “Step 0. Check your tools”Node 24 or newer:
node -vDocker Desktop must be running. This prints the server details, not “Cannot connect”:
docker infoVS Code’s code command. If it is not found, open VS Code, press Cmd+Shift+P and run Shell Command: Install ‘code’ command in PATH.
code --versionStep 1. Create the folders and open VS Code
Section titled “Step 1. Create the folders and open VS Code”mkdir -p ~/Desktop/tiny-shop/backend ~/Desktop/tiny-shop/frontend ~/Desktop/tiny-shop/stackcd ~/Desktop/tiny-shopcode .In VS Code, open a terminal with Ctrl+`. You will use three terminal tabs (the + button): one for the backend, one for the frontend, one for the stack.
Directorytiny-shop/
Directorybackend/ the API (this page, then page 3)
- …
Directoryfrontend/ the React page (this page, then page 4)
- …
Directorystack/ the ouo11y stack (page 2)
- …
Step 2. A simple API, no telemetry yet
Section titled “Step 2. A simple API, no telemetry yet”In the first terminal tab:
cd ~/Desktop/tiny-shop/backendnpm init -ynpm install express corsCreate backend/server.js:
const express = require("express");const cors = require("cors");
const app = express();app.use(cors());app.use(express.json());
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));const products = [ { id: 1, name: "Coffee", price: 4 }, { id: 2, name: "Tea", price: 3 }, { id: 3, name: "Cake", price: 6 },];
app.get("/api/products", async (req, res) => { await sleep(80); res.json(products);});
app.post("/api/checkout", async (req, res) => { await sleep(150); res.json({ ok: true, orderId: Math.floor(Math.random() * 100000) });});
app.get("/api/fail", (req, res) => { res.status(500).json({ error: "inventory service unavailable" });});
app.listen(5001, () => console.log("tiny-shop API on http://localhost:5001"));This is the first version of the file. Page 3 replaces it with the traced one.
Run it:
node server.jsIn another terminal tab, check that it answers:
curl localhost:5001/api/productsYou get the three products as JSON. (A % at the end is zsh saying the output had no final newline; it is not part of the answer.) Leave the API running.
Step 3. A dummy React UI
Section titled “Step 3. A dummy React UI”In the second terminal tab:
cd ~/Desktop/tiny-shopnpm create vite@latest frontend -- --template react --no-interactivecd frontendnpm installReplace frontend/src/App.jsx with:
import { useEffect, useState } from "react";
const API = "http://localhost:5001";
export default function App() { const [products, setProducts] = useState([]); const [cart, setCart] = useState([]); const [message, setMessage] = useState("");
useEffect(() => { fetch(`${API}/api/products`).then((r) => r.json()).then(setProducts); }, []);
async function checkout() { const res = await fetch(`${API}/api/checkout`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ items: cart.map((p) => p.id) }), }); const data = await res.json(); setMessage(`Order #${data.orderId} placed`); setCart([]); }
async function callBrokenApi() { const res = await fetch(`${API}/api/fail`); setMessage(`API answered ${res.status}`); }
function crash() { throw new Error("Button exploded"); }
return ( <main style={{ fontFamily: "sans-serif", maxWidth: 480, margin: "40px auto" }}> <h1>Tiny Shop</h1> <ul> {products.map((p) => ( <li key={p.id}> {p.name} (${p.price}) <button onClick={() => setCart([...cart, p])}>Add</button> </li> ))} </ul> <p>Cart: {cart.length} item(s)</p> <button onClick={checkout} disabled={!cart.length}>Checkout</button>{" "} <button onClick={callBrokenApi}>Call broken API</button>{" "} <button onClick={crash}>Throw error</button> <p>{message}</p> </main> );}This is the first version too. Page 4 replaces it with one that uses the browser SDK.
Start it:
npm run devWhat you should see
Section titled “What you should see”Open http://localhost:5173. You see the three products: Coffee, Tea and Cake (the page reached your API). Click Add, then Checkout: “Order #… placed”. Call broken API shows “API answered 500”, and Throw error only leaves Uncaught Error: Button exploded in the DevTools console, where nobody but you will ever see it. That is the error page 5 finds in Grafana.
The app works, and nothing is instrumented yet. Leave both running: the API in the first tab, Vite in the second.