Instrumenting an app · 4 min read · Updated 2026-09-22

Analytics in application code

Name the intent, fire it once, identify the person, and put the charge on the server — the browser never saw the card succeed.

Part 5 of 8 · Next: A checkout, end to end

Error tracking tells you the charge threw 40 times. It does not tell you that 3,000 people put a product in a cart last week and 700 paid. That gap is product analytics: named actions, in order, tied to a person. The SDK you already installed for exceptions is the same SDK. The envelope is not.

Concepts — visitor, session, funnel, privacy — are in the product analytics guides. This chapter is what you type on the example checkout. If your core action is signup, upgrade, or publish, keep the shape: a page or screen, a meaningful view, an intent, a start, a success or failure.

Project settings, then consent

Turn analytics on in Project settings. Browser SDKs still wait for optIn() after the visitor agrees. That is how you put the storefront behind a consent banner. A server call can record order_completed with a userId or anonymousId once the project allows analytics. See privacy and consent and the setup guide.

Typescript
Talaria.init({
  dsn: 'https://ingest.newtalaria.com',
  apiKey: process.env.NEXT_PUBLIC_TALARIA_KEY!,
  release: '1.4.2',
});

Talaria.analytics.optIn();

A checkout taxonomy

Start with six names. You can add more; you cannot cheaply rename an event the quarterly report already uses. Convention: object_verb in past tense, ids in properties. Full naming rules: events and properties.

  • $pageview — Talaria.analytics.page() on each storefront navigation. The SDK will do this for you on the web if you call page from the router; do not also invent page_viewed.
  • product_viewed — the product page is meaningful (/products/trail-runner), not a hover on a card.
  • add_to_cart — sku, price, size.
  • checkout_started — they committed to paying, not that the layout mounted twice in Strict Mode.
  • checkout_failed — the charge did not complete. Reason as a property (card_declined), not the exception message.
  • order_completed — server-side, after the charge and the row. This is the one the funnel exists for.

Event detail

storefront

production

Properties

{ sku: 'TR-900', price: 129, size: 'M' }

Identity

anonymousId
a7f3…c1
sessionId
s_9d2…4e
userId
user_123

What happened

name
add_to_cart
kind
track
timestamp
2026-09-21T09:14:02Z

Page context

path
/products/trail-runner
referrer
https://google.com
utmCampaign
spring-sale

Correlation

replayId
rpl_4c8…
traceId
4bf92f35…
release
1.4.2
add_to_cart for TR-900. Your properties at the top; identity, page, campaign, and correlation filled in by the SDK.
storefront/lib/analytics.tsTypescript
Talaria.analytics.page(); // $pageview

Talaria.analytics.track('product_viewed', {
  sku: 'TR-900',
  price: 129,
});

Talaria.analytics.track('add_to_cart', {
  sku: 'TR-900',
  price: 129,
  size: 'M',
});

Talaria.analytics.track('checkout_started', {
  value: 129,
  currency: 'USD',
  item_count: 1,
});

The charge the browser never saw

If you record order_completed in the browser, ad blockers, crashes, and “close the tab after Pay” will undercount the only number finance trusts. The API already knows the charge succeeded. Forward the browser’s anonymousId (and userId if present) on the checkout request so the server event stitches to the same person.

api/src/Checkout/CompleteOrder.phpPhp
Talaria::analytics()->track('order_completed', [
    'sku' => 'TR-900',
    'value' => 129,
    'currency' => 'USD',
    'order_id' => $order->id,
], [
    'anonymousId' => $request->header('X-Anonymous-Id'),
    'userId' => $buyer->id,
]);

PHP analytics has no cookie jar and no autocapture. If you omit identity options, the row is harder to join to the storefront session. Failed charges that you still want on the funnel fire checkout_failed here too — next to captureException, not instead of it.

Identify after login, not before

anonymousId is a browser. sessionId is a visit. userId is a person. identify binds the first to the third. Call it when you know who they are — after login, after account creation — with a stable application id. Calling it on every pageview does nothing useful and creates noise in the identify stream.

Typescript
Talaria.setUser({ id: user.id });
Talaria.analytics.identify(user.id, {
  plan: user.plan,
});

One visitor, three sessions, one person

storefront

production
  • Mon 09:02$pageviews_9d2…4eanonymous

    First visit. Visitor id created and stored first-party.

  • Mon 09:05add_to_carts_9d2…4eanonymous

    Same session — under 30 minutes since the last event.

  • Mon 11:40$pageviews_1b4…a8anonymous

    New session. Same visitor: the idle window expired.

  • Mon 11:42$identifys_1b4…a8user_123

    Signs in. Every later event carries the user id.

  • Tue 08:15$pageviews_66f…02user_123

    New UTC day, new session — still one unique person.

The same visitor, before and after identify. Uniques collapse onto the person; the anonymous id does not get rewritten out of history.

Do not pass email as a property — the server strips it. Do not use email as userId. Mechanics and the unique-user rule: visitors, sessions, and identify.

The funnel is five names

The checkout funnel is the sequence already drawn in Learn: $pageview → product_viewed → add_to_cart → checkout_started → order_completed. Session scope, one-day window. Every entrant who stops is a person you can open — replay, errors, trace. How the counts are computed: funnels and drop-off.

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 same five steps. The interesting number is never the last bar — it is which step lost 3,034 people.

Properties that survive a year

  • Scalars only — sku, price, size, currency, plan, reason. Nested objects stringify and will not filter the way you hope.
  • Low cardinality in the name, high cardinality in the property. product_viewed + { sku: 'TR-900' }, never product_TR900_viewed.
  • 64 keys, 1024 characters per string, 16 KB for the map or the map is dropped whole. No serialized carts. No stacks.
  • Secret keys (password, token, email, credit_card, …) never land. Do not try to outsmart the strip list.

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.

  • Product analytics · 3 min

    Events and properties

    Four kinds of call, one envelope, and a property bag you control. Naming is the only hard part.

  • Product analytics · 3 min

    Visitors, sessions, and identify

    Three ids doing three different jobs. Get them straight and every number downstream makes sense.

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