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

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.

Browser page and track, API transaction, Stripe client span, MySQL child.

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.

storefront/lib/talaria.tsTypescript
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();
api/public/index.phpPhp
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.

Typescript
// 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.

storefront/app/products/[slug]/view.tsxTypescript
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.

storefront/components/add-to-cart.tsxTypescript
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.

storefront/components/checkout-form.tsxTypescript
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.

storefront/lib/checkout.tsTypescript
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

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.

The header the storefront sent is the parent the API attaches to.
api/src/Checkout/ChargeOrder.phpPhp
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_cart and order_completed in top events, spring-sale in campaigns.
  • The checkout funnel — the five-step sequence. Drop-off between checkout_started and order_completed is 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 /checkout p95, a waterfall with the Stripe child and INSERT orders, error rate that moves when the gateway blinks.

Checkout funnel · 24h window

storefront

production
  1. 01 $pageview9,318 · 100%
  2. 02 product_viewed5,140 · 55.2%

    −4,178 dropped here

  3. 03 add_to_cart2,106 · 22.6%

    −3,034 dropped here

  4. 04 checkout_started1,284 · 13.8%

    −822 dropped here

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

The funnel you saved. Click a person who stopped at checkout_started and you are in their replay.

Trace

00-3a91… · 184 ms

production
  1. POST /checkout

  2. SELECT related

    N+1
  3. SELECT related

    N+1
  4. SELECT related

    N+1
  5. POST stripe.com

  6. INSERT orders

The matching API transaction. If p95 moved and errors did not, the long child is usually here.

The failed purchase, end to end

  1. Storefront: breadcrumb + checkout_started.
  2. Fetch carries traceparent and X-Anonymous-Id.
  3. API continues the trace, breadcrumb, Stripe span, exception, checkout_failed.
  4. 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 app

    Four 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 make

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

    Monday: 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-off

    A funnel tells you which step loses people. Talaria then hands you the people, and the replay of them leaving.

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.