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

production

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.

One purchase, four hops, the call that belongs on each.

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).

storefront/lib/talaria.tsTypescript
Talaria.init({
  dsn: 'https://ingest.newtalaria.com',
  apiKey: process.env.NEXT_PUBLIC_TALARIA_KEY!,
  release: '1.4.2',
});
api/public/index.phpPhp
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 server span, named like POST /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 client spans.
  • 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.

api/src/Checkout/ChargeOrder.phpPhp
$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();
}
Typescript
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 fills db.system.name and db.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

production

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.

Same 32-hex trace id on the pageload, the fetch, POST /checkout, and MySQL.

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 make

    The 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 spans

    A 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 Context

    One 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 explain

    Keep every failing transaction. Sample a tenth of the rest. Bill the root, not every query. That is the whole policy.

Instrument the first three events

Install an official SDK, opt into tracing and analytics, and name the start, the middle, and the finish of your core action.