Instrumenting an app · 6 min read · Updated 2026-09-22
A checkout, end to end
One purchase, every hop, the actual calls. This is what “instrumented” looks like when you are done.
Part 6 of 8 · Next: One incident
The previous chapters were rules. This one is a walk-through of the example checkout. A person lands from utm_campaign=spring-sale, opens /products/trail-runner, adds size M, pays, and either gets an order or a declined card. Every Talaria call that belongs on that path is below. If a line is not here, do not emit it. The same calls exist in Dart, Flutter, and Node — only the syntax changes.
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.
Init once per process
The storefront initializes in the browser client (and the Next.js server, if you render there) with tracing and analytics opt-in after consent. The API initializes in front-controller boot. Same release. The API key decides the environment. This example uses one project and a production key on the deployed storefront and API, so the W3C join is free. A local run uses a development key and still sends a version or SHA as release.
Talaria.init({
dsn: 'https://ingest.newtalaria.com',
apiKey: process.env.NEXT_PUBLIC_TALARIA_KEY!,
release: '1.4.2',
});
// After the consent banner:
Talaria.analytics.optIn();Talaria::init([
'dsn' => 'https://ingest.newtalaria.com',
'apiKey' => getenv('TALARIA_API_KEY'),
'release' => '1.4.2',
'minLevel' => 'warning',
]);Scene 1 — Landing
They arrive on / with UTM. The browser SDK starts a pageload transaction and records Web Vitals as they happen. You call page() once for the route. You do not track('landed'). You do not start a span named homepage. First-touch campaign fields attach to the session automatically — UTM and attribution.
// app router: once per pathname
Talaria.analytics.page();Scene 2 — Product page
Navigation to /products/trail-runner ends the previous idle transaction and starts a new one. Another page(). Then one meaningful product event — not on thumbnail hover.
Talaria.analytics.page();
Talaria.analytics.track('product_viewed', {
sku: 'TR-900',
price: 129,
});If they are signed in from an earlier visit, identify already ran this session — do not call it again on the product page. If the page is slow, you read Web Vitals and the pageload waterfall, not a custom span around the React tree.
Scene 3 — Add to cart
The click is an intent. It is also useful context if checkout later throws. Do both: a track for the funnel, a breadcrumb for the issue.
function onAddToCart(size: 'S' | 'M' | 'L') {
Talaria.addBreadcrumb({
type: 'user',
category: 'cart',
message: 'Added to cart',
data: { sku: 'TR-900', size },
});
Talaria.analytics.track('add_to_cart', {
sku: 'TR-900',
price: 129,
size,
});
}Scene 4 — Checkout opens
Mounting the form is checkout_started once — guard against Strict Mode double-invoke. A breadcrumb marks the same moment for the next exception. The fetch to POST /checkout will become a client span on its own.
useEffect(() => {
Talaria.addBreadcrumb({ type: 'user', message: 'Opened checkout' });
Talaria.analytics.track('checkout_started', {
value: 129,
currency: 'USD',
item_count: 1,
});
}, []);Scene 5 — Submit
The browser sends the cart, the anonymousId, and — automatically — traceparent. Optional client span if you do real work before fetch. Then the network hop is the story.
async function submit(cart: Cart, visitorId?: string) {
const span = Talaria.startSpan('ui.checkout.submit');
try {
const res = await fetch('https://api.example.com/checkout', {
method: 'POST',
headers: {
'content-type': 'application/json',
...(visitorId ? { 'X-Anonymous-Id': visitorId } : {}),
},
body: JSON.stringify({ sku: 'TR-900', size: cart.size }),
});
if (!res.ok) throw new Error('Checkout request failed');
return await res.json();
} catch (error) {
await Talaria.captureException(error, {
tags: { feature: 'checkout' },
extra: { sku: 'TR-900', cart_id: cart.id },
});
Talaria.analytics.track('checkout_failed', { reason: 'network' });
throw error;
} finally {
span?.end();
}
}Send the same first-party visitor id the SDK already stamped on add_to_cart — persist it in your app when the session starts — so the API can set anonymousId on order_completed. The browser SDK already attaches traceparent on the fetch.
Scene 6 — The API charges
Middleware already opened POST /checkout as the transaction and continued the incoming traceparent. You do not start a second root. You add a child for Stripe if the client is not auto-wrapped, let PDO record the insert, and you decide success or failure once.
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.
public function __invoke(ServerRequestInterface $request): ResponseInterface
{
$body = json_decode((string) $request->getBody(), true);
$anonymousId = $request->getHeaderLine('X-Anonymous-Id') ?: null;
$userId = $this->buyerId();
if ($userId) {
Talaria::analytics()->identify($userId, [], [
'anonymousId' => $anonymousId,
]);
}
Talaria::addBreadcrumb([
'type' => 'http',
'message' => 'Charging TR-900',
'data' => ['sku' => $body['sku'] ?? ''],
]);
$span = Talaria::startSpan('payments.stripe.charge', 'client', [
'peer.service' => 'stripe',
]);
try {
$charge = $this->stripe->charges->create([
'amount' => 12900,
'currency' => 'usd',
'source' => $body['token'],
]);
$span->setStatus('ok');
} catch (\Throwable $e) {
$span->setStatus('error');
$span->end();
Talaria::captureException($e, [
'tags' => ['feature' => 'checkout', 'gateway' => 'stripe'],
'extra' => ['sku' => 'TR-900'],
'userId' => $userId,
]);
Talaria::analytics()->track('checkout_failed', [
'sku' => 'TR-900',
'reason' => 'card_declined',
], [
'anonymousId' => $anonymousId,
'userId' => $userId,
]);
throw $e;
}
$span->end();
$order = $this->orders->insert('TR-900', $charge->id);
Talaria::analytics()->track('order_completed', [
'sku' => 'TR-900',
'value' => 129,
'currency' => 'USD',
'order_id' => $order->id,
], [
'anonymousId' => $anonymousId,
'userId' => $userId,
]);
return new JsonResponse(['orderId' => $order->id]);
}What you do not do in this action: Talaria::info('Checkout opened'), a track of the raw token, a transaction named with the order id, or a span per inventory row. PDO’s insert is already a child. Expected validation (“size missing”) is a 400 and maybe checkout_failed with reason: 'validation' — not captureException.
Do not put card numbers, tokens, or the raw Stripe source into extra, properties, or span attributes — strip lists are a backstop, not a design.
What the dashboard shows
After a good day and one bad card, you can open four surfaces without changing product:
- Analytics Overview — pageviews, uniques,
add_to_cartandorder_completedin top events,spring-salein campaigns. - The checkout funnel — the five-step sequence. Drop-off between
checkout_startedandorder_completedis a list of people, not a percentage you shrug at. - Issues —
CardException(or whatever Stripe threw) as one issue, breadcrumbs including “Opened checkout” and “Charging TR-900,” View trace and View replay when those ids exist. - Performance —
POST /checkoutp95, a waterfall with the Stripe child andINSERT orders, error rate that moves when the gateway blinks.
Checkout funnel · 24h window
storefront
- 01 $pageview9,318 · 100%
- 02 product_viewed5,140 · 55.2%
−4,178 dropped here
- 03 add_to_cart2,106 · 22.6%
−3,034 dropped here
- 04 checkout_started1,284 · 13.8%
−822 dropped here
- 05 order_completed742 · 8.0%
−542 dropped here
Entrants who never reached step 2 are listed by name, each with their replay, errors, and trace.
Trace
00-3a91… · 184 ms
POST /checkout
SELECT related
N+1SELECT related
N+1SELECT related
N+1POST stripe.com
INSERT orders
The failed purchase, end to end
- Storefront: breadcrumb +
checkout_started. - Fetch carries
traceparentandX-Anonymous-Id. - API continues the trace, breadcrumb, Stripe span, exception,
checkout_failed. - Browser replay retains because an error fired. The issue has both buttons. The funnel lost one person at the last step with a reason you can group.
Related guides
Instrumenting an app · 5 min
Instrumenting an appFour signals, one checkout, and a rule for every line of application code: capture, span, track, breadcrumb — or leave it alone.
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.
Instrumenting an app · 5 min
One incidentMonday: the funnel drops at checkout. You do not open three vendors. You follow the person, the replay, the issue, and the query.
Product analytics · 4 min
Funnels and drop-offA funnel tells you which step loses people. Talaria then hands you the people, and the replay of them leaving.