Instrumenting an app · 5 min read · Updated 2026-09-22
Errors in application code
Talaria events are exceptions and the rare message that should become an issue. Everything else is a breadcrumb, a span, or stdout.
Part 3 of 8 · Next: Tracing in application code
Your API already has a log file. The storefront already has the browser console. Those are fine. They are also not Talaria. The moment you treat the event stream as a drain for every info, two things happen: you pay for volume that will never be triaged, and the issue inbox drowns.
Examples in this chapter are from a checkout catch block. The same calls apply anywhere you handle a failure for the user and still need an issue. Grouping, replay, and the path from an issue to a waterfall live in From error to trace to replay. The decision table is in Which call to make.
What an event is
An event is one captured occurrence. Almost always that is an exception: a throwable with a stack, a level, tags, extra, a user, and — when tracing or replay is on — traceId and replayId. The server fingerprints it. Identical crashes become one issue with a count. Clients never compute that fingerprint.
A message event is the same envelope without a stack. It still becomes an issue if the fingerprint lands somewhere new. That is why “Checkout opened” as info is so expensive: it is not a log line you will grep later. It is a candidate issue.
captureException is the default
Unhandled exceptions should already flow through the SDK. You still have catch blocks — payment, inventory, the webhook — where you handle the error for the user and must not swallow the telemetry.
Talaria.addBreadcrumb({ type: 'user', message: 'Opened checkout' });
try {
await pay(cart);
} catch (error) {
await Talaria.captureException(error, {
tags: { feature: 'checkout' },
extra: { cart_id: cart.id, sku: 'TR-900' },
});
throw error;
}Talaria::addBreadcrumb([
'type' => 'http',
'message' => 'Charging TR-900',
]);
try {
$charge = $stripe->charges->create($payload);
} catch (\Throwable $e) {
Talaria::captureException($e, [
'tags' => ['feature' => 'checkout'],
'extra' => ['cart_id' => $cart->id, 'sku' => 'TR-900'],
]);
throw $e;
}Re-throw after capture unless you are at the process boundary. Swallowing hides the user-facing failure and still creates the issue — the worst of both. Tags are string → string and should be low-cardinality (feature, gateway). Extra is for ids that explain this occurrence, not the whole cart JSON.
PHP loggers
The PHP SDK is a PSR-3 logger. Talaria::error() with an exception key is captureException through the logger — the right adapter for existing Laravel / Silverstripe logs you already trust. Talaria::info() is not. Set minLevel to warning or error in production unless a named logger is deliberately louder.
$logger = Talaria::logger('checkout');
$logger->error('Charge failed', [
'exception' => $e,
'tags' => ['feature' => 'checkout'],
'cart_id' => $cart->id,
]);Scoped loggers inherit the client minLevel and may go more verbose for one module. Do not set the root to debug in production to make one handler chatty. Official SDKs also sample events client-side after the level gate; that is a safety net, not a license to emit.
When captureMessage is justified
A message event is right when all three are true:
- There is no throwable — the failure is a branch, not an exception.
- You would open an issue if it happened fifty times — dead-letter, signature mismatch, “inventory said yes and the warehouse said no.”
- It is not a success path. Successes are analytics.
if (!$verified) {
Talaria::captureMessage(
'Stripe webhook signature failed',
'error',
['tags' => ['feature' => 'billing'], 'extra' => ['hook' => $type]],
);
return $response->withStatus(400);
}One event per failed verification, not one per retry loop. If the same webhook retries, prefer capturing on the terminal failure.
Breadcrumbs are the trail
Breadcrumbs are the answer to “what happened just before this?” Navigation, console, network, and a few manual marks: opened checkout, selected size M, applied spring-sale. They attach to the next event and cost nothing on the event meter. They are discarded if nothing fails.
They are not product analytics. You cannot funnel on a breadcrumb. You cannot count unique people from a breadcrumb. If you will ask “how many people applied a code,” that is track('coupon_applied').
Talaria.addBreadcrumb({
type: 'user',
category: 'cart',
message: 'Selected size',
data: { sku: 'TR-900', size: 'M' },
});User, environment, release
Set the user when you know them — after login, after identify. Use a stable application id, not an email (emails get stripped on analytics properties and should not be your join key anyway). Environment is development, test, staging, or production, and the API key decides it. Release is the git SHA or the SemVer you ship, the same string the browser and the API send, so release health can tell you that 1.4.2 is the broken one.
Browser replay is error-triggered by default. You do not record every session. When an exception fires and a recording was retained, the event carries replayId. The API never records a session — there is no DOM.
What belongs in stdout
- Request/response dumps while you reproduce a bug locally.
- High-volume access logs and CDN logs.
- Health checks, cron “still alive,” queue heartbeats.
- Anything you would only grep from a file or a log product.
Keep those streams. Point them at whatever you already pay for. Do not dual-write them to Talaria “just so it is in one place.” One place for issues is the point.
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.
Triage loop · 3 min
From error to trace to replayPerformance without grouping is a chart. Grouping without traces is a stack. Replay without either is a video. Talaria joins all three on one issue.
Instrumenting an app · 4 min
Tracing in application codeA 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.