Instrumenting an app · 4 min read · Updated 2026-09-22
Tracing in application code
A transaction is the request. A span is a timed child. You name the route, you opt in, and you let auto instrumentation do the dull work.
Part 4 of 8 · Next: Analytics in application code
When checkout is slow, nobody needs a CPU chart first. They need to know whether the time went into Stripe, into INSERT orders, or into the browser waiting on a 1.8s LCP. That tree is a trace. The root of the tree is a transaction — the unit Talaria meters. Child spans travel with it and are not a second bill.
Definitions, span kinds, and semantic conventions are in Transactions, traces, and spans. This chapter is where the calls live. Snippets stay on the example checkout; the same APIs exist in every official SDK.
Call sites on the path
One purchase
Browser
/products/trail-runner
page · track · pageload
API
POST /checkout
transaction · breadcrumb
Stripe
charges.create
client span
MySQL
INSERT orders
db span
Charge success is order_completed on the server. The exception, if any, rides the same traceId.
Tracing follows the project
Official SDKs do not emit spans until tracing is on in Project settings. Turn it on per project when the dashboard is ready, not in the same commit that adds the SDK for errors. The SDK applies the change on its next refresh (about five minutes).
Talaria.init({
dsn: 'https://ingest.newtalaria.com',
apiKey: process.env.NEXT_PUBLIC_TALARIA_KEY!,
release: '1.4.2',
});Talaria::init([
'dsn' => 'https://ingest.newtalaria.com',
'apiKey' => getenv('TALARIA_API_KEY'),
'release' => '1.4.2',
]);Error transactions are always sampled once tracing is on. Successful ones default to 10%. That is how you keep a waterfall for every payment exception without paying to store every happy GET /products/trail-runner. Details in sampling and metering.
This course uses native SDKs. Teams already running a collector can send OTLP/HTTP JSON traces to /otlp/v1/traces instead.
Auto first, manual when it is not a request
What official SDKs already start as transactions:
- PHP / Laravel / Silverstripe — middleware wraps the inbound request as a
serverspan, named likePOST /checkout. - Browser — a pageload transaction that ends on idle (last meaningful child), plus a new transaction on each History / App Router navigation. Fetch and XHR become
clientspans. - PHP children — PDO / Silverstripe MySQL, Guzzle, Redis, queued jobs when those integrations are present.
You write a manual span when the work is real and invisible to those integrations: a Stripe SDK that does not go through Guzzle, an in-process pricing engine, a batch of inventory locks. You do not write a manual span around work the middleware already timed.
$span = Talaria::startSpan('payments.stripe.charge', 'client', [
'peer.service' => 'stripe',
]);
try {
$charge = $stripe->charges->create($payload);
$span->setStatus('ok');
return $charge;
} catch (\Throwable $e) {
$span->setStatus('error');
throw $e;
} finally {
$span->end();
}const span = Talaria.startSpan('ui.checkout.submit');
try {
await postCheckout(cart);
} finally {
span?.end();
}The browser ui.checkout.submit span is optional once the fetch to /checkout is already a child of the page transaction. Add it when you need to see client-side validation or encryption time that never hits the network.
Never wrap Talaria ingest
If you add a global fetch / Guzzle middleware that starts a span for every HTTP call, exclude Talaria’s own ingest URLs first. Tracing the tracer is how you get recursive waterfalls and a very confusing bill. Official SDKs already skip those URLs.
Name the route, not the URL
POST /checkout is a name you can sort. POST /checkout/cart_01H8Z… is a unique transaction every time and a useless p95. Same rule as analytics names: low cardinality in the name, ids in attributes.
- HTTP — method plus route template:
GET /products/:slug,POST /checkout. - DB — operation plus table when you set it by hand:
INSERT orders. Auto instrumentation already fillsdb.system.nameanddb.query.text. - Peers —
peer.service=stripe, not the full URL with a request id.
N+1 is a query pattern, not a span you add. Turn tracing on, hit the product page, and open slow queries and N+1. If related products load with one query per card, the view will say so. The fix is eager load, not more instrumentation.
Browser to API is one trace
The storefront and the API are two processes. They become one trace when the browser fetch sends W3C traceparent and the API continues it. Official browser SDKs attach the header to same-project API calls. Official PHP SDKs read it. You do not mint a traceId by hand.
Trace context
W3C traceparent
00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
Browser
pageload + fetch
API
POST /checkout
MySQL
INSERT orders
Same trace ID on every hop. The span ID in the header is the parent the next service attaches to.
If the API is on another origin, allow the header in CORS. If you terminate TLS in a proxy that strips unknown headers, add traceparent to the allowlist. When the join is working, a checkout exception in the API opens a waterfall that starts in the browser. When it is not, you have two orphans and a hunch. Full mechanics: W3C Trace Context.
Errors mark the transaction
When captureException runs while a transaction is open, the SDK marks that root as an error and keeps the sample. That is why payment throws are always in Performance even at a 10% success rate. After the pageload has already gone idle, the event still gets the last traceId — the SDK does not reopen a finished waterfall to stick more time on it.
Related guides
Instrumenting an app · 5 min
Which call to makeThe chapter you keep open while you write. A failure, an intent, a duration, or context — pick one call, or pick none.
Signals · 3 min
Transactions, traces, and spansA trace is one request’s path. A span is a timed unit of work. A transaction is the root span Talaria meters.
In the request · 2 min
Distributed tracing with W3C Trace ContextOne trace ID from the browser, through the API, into the query that failed — carried on a standard header named traceparent.
Signals · 3 min
Sampling you can explainKeep every failing transaction. Sample a tenth of the rest. Bill the root, not every query. That is the whole policy.