Skip to content

The browser: a custom event

Any event.name that is not one of the six in the contract is a custom event. ouo11y shows it on the session’s timeline and never counts it in health. addEvent() from events.js makes one:

addEvent("cart.added", { "product.id": 1 });

Replace frontend/src/App.jsx with the final version. Compared with step 3, the API URL comes from telemetry/config.js (the same value the fetch instrumentation matches), and Add records a custom cart.added event:

frontend/src/App.jsx
frontend/src/App.jsx
import { useEffect, useState } from "react";
import { API, addEvent } from "./telemetry";
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);
}, []);
function addToCart(p) {
setCart([...cart, p]);
// A custom event: a log record with event.name "cart.added" in Loki.
addEvent("cart.added", { "product.id": p.id, "product.name": p.name });
}
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() {
// A 500: recorded as http.request.failed in the browser and as an error span in the API.
const res = await fetch(`${API}/api/fail`);
setMessage(`API answered ${res.status}`);
}
function crash() {
// An uncaught error: an exception event, and the session is marked crashed.
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={() => addToCart(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>
);
}

That is the last file. Your frontend/src/telemetry/ folder now holds the six files of the finished quickstart, about 400 lines in all.

Vite updates the page on its own. Click Add on a product. In Grafana, Explore, Loki, within a few seconds:

{service_name="tiny-shop-otel-frontend", event_name="cart.added"}

One line per click, cart.added {"product.id":1,"product.name":"Coffee"}, with the session’s session_id.

Next: see the full picture.