Skip to content

Labels and filters

Write logger.error(err, { orderId }) and the entry also has the user, screen, release, browser and the last 50 breadcrumbs.

Frontend and backend write to the same stream in the same shape, so one filter reads the whole story in time order:

labels.userId="<uid>"
10:42:03.114 INFO screen=checkout click "Apply code"
10:42:03.118 INFO screen=checkout applying discount SAVE20
10:42:03.402 INFO fn=applyDiscount started
10:42:03.611 ERROR fn=applyDiscount coupon lookup failed: timeout
10:42:03.798 ERROR screen=checkout TypeError at Checkout.tsx:42:9

Two of those lines came from a browser and three from a server. Every label on them was attached automatically.

Field Added by Where it comes from
appId, releaseId client your initLogger config
screen client tracked as the user moves
route, path client with navigation on: the route pattern and the real path
view client with views on: the marked tabs, steps and dialogs visible
userId client setUser, held for the session
platform client user agent: ios, android, macos, windows, linux or web
browser client user agent
errorType client the Error’s own name
last 50 breadcrumbs client the trail of what the user did
logId function a ULID, unique per entry, which locates attachments in Cloud Storage
hasAttachments function "true" when files were uploaded alongside
resolved file and line function your source maps: Checkout.tsx:42:9, not app-4f2a.js:1:98432
trace context function request correlation in Cloud Logging

The backend adds functionName and userId through withLogging, from the verified request.auth.uid. The client attaches the uid from setUser. Both use the label name userId, so the query above returns both halves without any coordination.

The rule behind it: you never pass context to a log call. You declare it once, and fsl adds it to every entry. On the client that scope is the session. On the backend it is the request.

src/main.ts
import { initLogger } from '@dasasian/firebase-structured-logger/client'
import { httpsCallable } from 'firebase/functions'
import { functions } from './config/firebase'
export const logger = initLogger({
appId: 'my-app',
releaseId: import.meta.env.VITE_RELEASE_ID ?? 'dev',
logFunction: httpsCallable(functions, 'logFrontendEvent'),
})
logger.setUser('uid-123', { organizationId: 'org-9' })
logger.error(new Error('sync failed'), { orderId: 'o-1042' })

initLogger applies to every log for the life of the app. setUser applies until clearUser(). The label on the call applies to that one entry. The innermost wins: a label passed at the call site overrides the same label from setUser.

Define the label type once, in a file both the app and functions/ import:

src/shared/labels.ts
export type MyAppLabels = {
organizationId?: string
itemId?: string
}

On the client, pass it as the type argument and set it on sign in:

src/main.ts
import { initLogger } from '@dasasian/firebase-structured-logger/client'
import { httpsCallable } from 'firebase/functions'
import { functions } from './config/firebase'
import type { MyAppLabels } from './shared/labels'
export const logger = initLogger<MyAppLabels>({
appId: 'my-app',
releaseId: import.meta.env.VITE_RELEASE_ID ?? 'dev',
logFunction: httpsCallable(functions, 'logFrontendEvent'),
})
export function onSignIn(uid: string, organizationId: string) {
logger.setUser(uid, { organizationId })
}
export function onSignOut() {
logger.clearUser()
}

On the backend, derive them from the request:

functions/src/index.ts
import { onCall } from 'firebase-functions/v2/https'
import { withLogging, logInfo } from '@dasasian/firebase-structured-logger/functions'
import type { MyAppLabels } from '../../src/shared/labels'
export const checkout = onCall(
withLogging<MyAppLabels>(
(request) => ({ functionName: 'checkout', labels: { organizationId: request.data.orgId } }),
async () => {
logInfo('started')
},
),
)

Label values are strings. Keep them to identifiers you would filter on, not free text.

Narrow the user query when you need to:

Filter Returns
labels.platform:* client entries only
labels.functionName:* server entries only
labels.releaseId="<sha>" one build
labels.screen="checkout" one screen
labels.route="/orders/:id/items" one route, every id (with enableNavigation)
labels.path="/orders/1042/items" one real page: that order, that user (with enableNavigation)
labels.feedback="true" user-reported issues
labels.hasAttachments="true" entries with files in Cloud Storage
labels.truncated="true" entries shortened to fit. The full copy is fsl-overflow.json
labels.repeatKey="<key>" OR labels.repeatOf="<key>" one repeating error: its full copies and its summaries
labels.sentLate="true" repeat summaries sent on a later visit
labels.trace="app_boot" slow runs of one trace. labels.slow says whether the trace or a step was late

npx fsl logs schema lists every label key in your recent logs, how many entries have it and sample values. It reads the last 500 entries. userId, and any key you added whose name contains email, name or phone, is listed with a count and no sample values.

Terminal window
npx fsl logs schema
npx fsl logs schema --add venueId "the venue the order belongs to"

--add records labels your code can write, so the next reader knows they exist. The CLI is covered in fsl logs.

Made by Dasasian