Skip to content

Breadcrumbs

A stack trace says where the code broke. With breadcrumbs, the entry also says Checkout · apply_discount · total_recalculated · tap_place_order.

A stack trace cannot tell you what the person did to get there, which is usually the part you need to reproduce it. Breadcrumbs are that trail: a rolling record of the last steps, attached to every error and every piece of feedback with no correlation work on your side.

The trail holds at most 50 entries and nothing older than 5 minutes. That covers the steps that led here and stays the current attempt rather than the whole session. It lives in memory only, so it never touches storage and never leaves the device except attached to a log you send. It is one trail per session: one user, one path. In the entry, the trail is the jsonPayload.breadcrumbs list.

import { bc } from '@dasasian/firebase-structured-logger/client'
bc.action('apply_discount', { code: 'SAVE10' })
bc.state('total_recalculated', { total: 42.0 })
bc.handledError('draft_save_failed', { attempt: 1 })

Put a bc.action before anything that can fail: a click handler that awaits a request, a submit, a callable. Use bc.state for a result worth knowing, such as a recalculated total.

bc.handledError is for an error your code handled and chose not to log, like a save that failed and then worked on retry. It costs nothing unless something else goes wrong, and then the trail shows it as a clue. An error you do log is already in the logs, in time order with everything else, so it needs no breadcrumb.

For a single-page app, one call records every page change:

src/main.ts
import { enableNavigation } from '@dasasian/firebase-structured-logger/client/navigation'
enableNavigation()

It is its own entry point, so an app that never imports it ships none of it. It wraps history.pushState and replaceState, the only way to notice a single-page app changing route. Call it once, before or after initLogger. It records the current page at once and every change after it, back and forward included.

Each page change is one nav breadcrumb, and every entry has three labels:

Label Example Use it for
route /orders/:id/items grouping and counting
path /orders/1042/items finding the errors for order 1042
screen OrderItems, or the route when there is no name the page’s name

By default, a path segment that is all digits, a UUID, a hex string of 16 characters or more, or a ULID becomes :id. Anything else is kept as written, so /blog/my-post stays as it is. screen is the route.

labelsFor receives the real path and returns the labels for it. Whatever it returns is used as returned, and a field it leaves out is not logged.

src/main.ts
import {
defaultLabelsFor,
enableNavigation,
} from '@dasasian/firebase-structured-logger/client/navigation'
enableNavigation({
labelsFor: (path) => ({
...defaultLabelsFor(path),
screen: path.startsWith('/admin') ? 'Admin' : undefined,
}),
})

It must be synchronous, because it runs inside your router’s own pushState. If it throws, that page gets defaultLabelsFor(path) and the console says so once.

If your paths can hold personal data, clean them in the same function:

enableNavigation({
labelsFor: (path) => {
const labels = defaultLabelsFor(path)
return { ...labels, path: labels.path?.replace(/[^/]+@[^/]+/g, ':email') }
},
})

The query string, always. It is where tokens and emails usually ride. The fragment too, unless it is a route: #/orders/1042 is read as the path, while #section-3 and #access_token=… are dropped. labelsFor receives the path already stripped.

A wizard or a tab shell that never touches the address bar needs a call on each change:

import { navigatedTo } from '@dasasian/firebase-structured-logger/client/navigation'
navigatedTo('Checkout')

It records the same breadcrumb and labels. An optional second argument takes { route, path }. Do not call it for a change that also changes the URL while navigation is on: one page change is one breadcrumb. For a dialog, tab or step inside a page, use Tabs and dialogs.

React Router and Vue Router each have an adapter that reads the pattern and the page name from the router instead of guessing from the path, and logs loader and guard errors. labelsFor does not apply to them. Setup is on the React and Vue pages.

Made by Dasasian