Skip to content

Get started

Four steps take a browser error from app-4f2a.js:1:98432 to Checkout.tsx:42:9 in your own Google Cloud project.

A browser cannot write to Cloud Logging, so the setup has two halves: a client logger in your app, and a log function in your Cloud Functions that the client calls. The function holds your source maps and writes the entry. Both halves ship in one package.

  • Node 22 or newer in the backend.
  • A functions/ directory (firebase init functions, TypeScript) referenced by firebase.json.
  • Firebase Storage enabled, because source maps are uploaded there. In the Firebase console, open Storage and choose Get started.
  • A frontend build that emits source maps. The upload tool assumes Vite.

Backend on Cloud Run or another Node server instead of Cloud Functions? The browser half is the same. See Cloud Run for the server side.

Terminal window
# frontend (project root)
npm install @dasasian/firebase-structured-logger
# Cloud Functions
cd functions && npm install @dasasian/firebase-structured-logger

The package ships CommonJS with TypeScript types. You import from three entry points, /client for the browser, /functions for the backend and /testing for your tests, and you run the fsl command. firebase, firebase-admin and firebase-functions are optional peer dependencies, so bring your own versions.

At your app entry, before any logging:

src/main.ts
import { initLogger, setupGlobalErrorHandler } 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'),
})
setupGlobalErrorHandler()

setupGlobalErrorHandler() captures uncaught errors and unhandled rejections. logFunction is any function of the form (payload) => Promise<unknown>. httpsCallable() fits it, and so does a fetch to your own endpoint.

functions/src/index.ts
import { initLogger, createClientLogFunction } from '@dasasian/firebase-structured-logger/functions'
initLogger({ appId: 'my-app' })
export const logFrontendEvent = createClientLogFunction({
bucket: 'my-app.firebasestorage.app',
})

The bucket holds source maps and attachments, under two default prefixes: sourcemaps/{releaseId}/ and logAttachments/{logId}/. Leave bucket out and both fall back to your project’s default bucket.

Add the map upload between the build and firebase deploy. Keep any flags you already pass, such as --project.

package.json
"deploy": "export VITE_RELEASE_ID=$(git rev-parse --short HEAD) && npm run build && npx fsl upload-sourcemaps --backend=./functions --embed-sourcemaps && firebase deploy"

The script sets the release id once, so the build and the upload cannot disagree. Locally the id falls back to 'dev' and nothing is uploaded. Source maps covers what the upload does and how to change the bucket.

Send three test entries from the browser after a deploy:

import { sendTestLog } from '@dasasian/firebase-structured-logger/client'
sendTestLog()

It sends one error, one warning and one info. In production only WARNING and above are sent by default, so the info is dropped. Open the Logs Explorer in the Google Cloud console and paste this filter:

labels.errorType="fsl-verify"

The error’s stack should name a .tsx or .ts file with its line. If it still names a minified bundle, the maps were not found. If nothing arrives, nothing else here will work either, so fix this first. Wire sendTestLog() to a dev-only button.

  • Source maps: release ids, buckets, and keeping .map files off hosting.
  • React or Vue: errors your framework catches, and router adapters.
  • Cloud Run: a backend that is not Cloud Functions.

Made by Dasasian