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 SAVE2010:42:03.402 INFO fn=applyDiscount started10:42:03.611 ERROR fn=applyDiscount coupon lookup failed: timeout10:42:03.798 ERROR screen=checkout TypeError at Checkout.tsx:42:9Two of those lines came from a browser and three from a server. Every label on them was attached automatically.
What you get for free
Section titled “What you get for free”| 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.
Three scopes that merge
Section titled “Three scopes that merge”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.
Add your own labels
Section titled “Add your own labels”Define the label type once, in a file both the app and functions/ import:
export type MyAppLabels = { organizationId?: string itemId?: string}On the client, pass it as the type argument and set it on sign in:
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:
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.
Filters
Section titled “Filters”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 |
Find what labels exist
Section titled “Find what labels exist”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.
npx fsl logs schemanpx 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