What a trace shows you that a log can't
By Engineering"GET /checkout — 2,140ms". You've seen that line. It's true, and almost useless: it tells you something was slow without telling you what, and it stops at the boundary of whichever process wrote it.
A trace is the same request with structure. Instead of one duration you get a tree of spans — each with its own start, end, and attributes — so the 2,140ms decomposes into the work that actually consumed it.
What the SDK records for free
Set tracesSampleRate above 0 and the browser side needs no further code:
- A pageload transaction on first load, with TTFB, DOM-content-loaded, load time and transfer size as attributes.
- A navigation transaction per SPA route change, held open briefly so the burst of requests it triggers nests underneath it.
- An http.client span per fetch and XHR, named by method and path, tagged with the response status and marked errored on 4xx/5xx.
- A span per asset — scripts, images, fonts — with transfer and decoded size, when resourceTiming is on.
That alone answers the frontend half of the question: was the page slow because the server was slow, because one request blocked everything, or because you shipped a 2MB font?
Crossing the boundary
The interesting cause is usually on the other side. The SDK injects a W3C traceparent header on same-origin requests — same-origin by default on purpose, because sending trace ids to third parties triggers CORS preflights and leaks them. Wrap the handler on the server and its spans join the same trace:
import { wrapRouteHandler, withMarsad } from "@marsadco/sdk/nextjs";
export const GET = wrapRouteHandler(async (req) => { /* … */ });
export const submit = withMarsad(async (data: FormData) => { /* … */ }, "submit");Now the waterfall spans both processes. The click, the fetch it triggered, the route handler that served it, and the database call inside it are one picture — and the gap that owns the latency is the widest bar.
Sampling that keeps traces whole
Sampling is decided once, at the root of a trace, and children inherit the decision. A trace is recorded whole or not at all — you never open a waterfall to find the middle missing. Start at tracesSampleRate: 1.0 in development and lower it in production; traces are the most expensive thing the SDK can send.
Wrap your own work with startSpan and it nests into whatever is already active:
const user = await startSpan({ op: "db.query", name: "load user" }, async () =>
db.users.find(id),
);Every event captured while a span is active also carries its trace id — so an error found on the Issues page links back to the exact trace it happened in.
Start watching your apps in minutes
Drop in the SDK and watch your first events arrive. Free plan, no credit card.
Start free