Repository navigation
Releases
1.5.0
Added
fsl logs --allreads every line of the service. By default a cloud query now reads only the entries fsl wrote (see Changed);--allleaves that out, for a failure where fsl wrote nothing: out of memory, a crash at startup. It cannot be combined with--local.fsl logsandfsl logs attachmentssign in with a key file. Name it inGOOGLE_APPLICATION_CREDENTIALS(a.env.localis loaded).fslhands it to its owngcloudprocess asCLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDEand leaves the person'sgcloudlogin alone. A machine that already setsGOOGLE_APPLICATION_CREDENTIALSfor another purpose now hasfsl logsuse that key. With a key file named, nothing falls back to the personal login; a permission failure names the variable and the role (roles/logging.viewer, or read access to the bucket for attachments), and a sign-in failure with no key file names the key-file way.fsl doctorreports where it found the backend.--jsonhas one new field,setup.backendFrom:"flag","firebase.json","search"ornull. The Setup line adds "(found by search)" to a backend that the search found, so a person can see the deduction and correct it with--backend.enableActions()records marked user actions. From@dasasian/firebase-structured-logger/client/actions, an element markeddata-fsl-action="apply_discount"becomes anactionbreadcrumb on click, submit (<form>) or change (<select>,<input>,<textarea>), with no code per handler, so it also tells repeats apart by path.
Changed
- Every deprecated name has its own Mistakes line in
CAPABILITIES.md, with its replacement, and a test fails when one has none.ClientLogRequesthad no line. Six renamed options shared one line in one section, and the lines forbc.navandsetScreennamed no replacement. Each name now has a line in the section of the code that takes it, with the replacement the run-time warning names, so/fsl-reviewfinds it where it reads. The test takes the names from the list the deprecation tests use. - The backend writes the local log file in development on any server, and never in a test run. An app that passes
logLocalDirto the/functionsinitLoggerand runs off Cloud Functions, started withtsx watchornodeon a developer's machine, getsdev.jsonlthere, andfsl logs --localreads it. The file is written whenK_SERVICEis unset andNODE_ENVis neitherproductionnortest: Jest and Vitest setNODE_ENV=test, so a test run writes no file and makes no folder. A test runner that sets noNODE_ENV(Mocha,node --test) needsNODE_ENV=testin the test script. What the process prints does not change. A live server off Google Cloud that never setsNODE_ENV=productionwrites the file too, bounded by the record and rotation limits. Nothing changes for an app on Cloud Functions or in the emulator. fsl logsreads the entries fsl wrote. The Cloud Logging filter asks for entries that carry theappIdlabel, for the query,schemaand--repeats, so a service's own request lines no longer fill the 5000-entry scan. Pass--allto read every line. A cloud query that finds nothing says on stderr that--allalso reads lines fsl did not write.- A word
fsl logsdoes not know is an error.fsl logs --local schemaused to run a plain query and print "0 entries matched"; it now exits 1 and showsfsl logs schema --local. Any other stray word exits 1 and names the two sub-commands.schemaandattachmentsrefuse a stray word too. - A key file that is not there is an error in
fsl upload-sourcemaps.FIREBASE_SERVICE_ACCOUNT_PATHnaming a missing file used to upload with the personal application-default login without a word. It now exits 1, names the variable and the path, and sends nothing.GOOGLE_APPLICATION_CREDENTIALSis read the same way. fsl doctorfinds the backend by flag, then thefunctionskey offirebase.json, then a search. The search looks at the project root and each folder one level down whosepackage.jsonlists this package, drops a folder that holds a web build, and answers only when the web build is known and one folder is left. With none left the backend is not checked; with several, or with one and no known web build, doctor reportscould-not-checknaming the folders and the flags to pass.--backendand--distnow work next to afirebase.json. A project whosefirebase.jsonhas afunctionskey gets the same report as before, plusbackendFrom./fsl-reviewand/fsl-logsrun thefslof the installed package by its path.npx fslin a folder with no install reaches for another author's package namedfsl./fsl-reviewlooks one level down when the project root has no install.createClientLogFunction()andcreateClientLogHandler()take no argument when every default fits. The config was a required parameter, so the shortest correct call wascreateClientLogFunction({}). A call with a config works as before.- The docs moved to
https://fsl.dasasian.com/. The README is now a front page with a quick start and a map of the docs, and the npmhomepagepoints at the site. withLogginglogs what the handler throws, then throws it again. After the upgrade an app gets new entries: every throw out of a wrapped handler is logged with the request'sfunctionName,userIdand labels, and the caller, the schedule or the task queue gets the same value as before. A handler needs notry/catchonly to log; an error already given tologErroris not logged twice. A plainErroror an error with a status of 500 or more is anERROR; anHttpsErrorwith a 4xx code is aWARNINGwith its code and status and no stack. A plainErrorfrom a callable now shows as twoERRORentries: fsl's, with the labels, and Firebase's ownUnhandled error.- Repeats are told apart by the user's last actions on the screen. An error's repeat signature now includes the names of the last 3
bc.actionbreadcrumbs since the last navigation, so the same error reached by two different paths on one screen gets its own full copies and its own summary. An app with no action breadcrumbs gets the signature it always had. /fsl-reviewproposes by user flow when a capability fits many places, and asks which flows matter. Past about ten places it groups them by what the user is trying to do, gives the count for each flow, names the flow it would do first, and ends with one question.CAPABILITIES.mdsays which controls to mark first: payment, sign-in and saving work. The review still proposes and stops./fsl-reviewlists what the code has before it decides, says what it read, and reads comments before it proposes a removal. The review lists each component file and each block rendered on a condition, then decides for each which capability covers it, or none; the words in "Fits when" are no longer the limit of the search. The proposal says in one line how many files were read and the kinds of place looked for. When the code says why a line is there, the proposal gives both sides and does not call its removal a fix.CAPABILITIES.mdknows more places for screens and views. Pages without a URL change fit a Vuev-if/v-else-if/v-elsechain and a React tree swapped by early returns on auth or gate state. Views fitrole="menu",role="alertdialog",<Teleport,createPortal(and hand-made overlays; the section says to mark dialogs, overlays and the layout mode first, and warns against marking regions that are always visible together.- The browser logger adds an
errorbreadcrumb for every error that reaches it. Its name is the entry'serrorTypeand it has no data. It is added whether the rate limiter sends the error, counts it as a duplicate or refuses it, and not again while the newest breadcrumb is anerrorwith the same name. The trail of every later entry, feedback included, now shows the errors that reached the logger before it, with no code in the app; the entry's own trail does not hold its own. A repeat summary, feedback and entries with no error add none. After the upgrade, trails holderrorbreadcrumbs the app did not add, and an app that added its ownbc.handledError(next tologger.error(gets two until it removes its line.
Deprecated
FIREBASE_SERVICE_ACCOUNT_PATH. UseGOOGLE_APPLICATION_CREDENTIALS, with the same path. The old name still works, warns once, wins when both are set, and is removed in 2.0.
Removed
sendPendingSummaries()is gone from the client logger's type. It was never documented, and a call from an app sent nothing useful. fsl sends repeat summaries itself: once afterinitLogger, hourly, and when the tab is hidden. No code change is needed in an app.
Fixed
- The rate limiter limits where browser storage fails. With no
sessionStorageorlocalStorage(a private window, a sandboxed iframe, an app's tests in Node with no DOM), or storage that throws on a read or a write (a full quota), each log started from a fresh budget: the same error twenty times sent twenty entries, where a working browser sends three and counts the rest. From the first read or write that fails, that store's state (the budget and duplicate counts, or the summary queue) now lives in memory for the rest of the page load; a reload loses it. A working storage is used as before, andresetSession()clears the memory state too. - A flag's value comes after
=or after a space infsl doctorandfsl upload-sourcemaps.fsl doctor --backend backendused to read the folder astrue;--backend,--dist,--functions,--bucket,--prefixand--releasenow take the next word, and a value flag with no value exits 1 and shows the correct form.--json,--strict,--force,--globaland--embed-sourcemapsstay switches and never take the next word. - **A cut
fsl logssca...
1.4.0
Tools for agents. fsl logs lets a coding agent read production logs with flags named
after SQL clauses, one JSON entry per line, and an error that names the valid fields when
it guesses one. /fsl-review reads CAPABILITIES.md from the installed package and
proposes how fsl could serve the code in front of it. Errors a framework catches are now
logged where it catches them, so a React boundary or a router no longer hides them. And
withLogging wraps scheduled and task handlers too. A production app ran each release
candidate before this release.
Added
withLoggingwrapsonScheduleandonTaskDispatchedas well asonCall: passScheduledEventorRequest<Data>as the second type argument.userIdcomes fromauth.uidwhen the event has one.CAPABILITIES.mdno longer listsonRequest(, whichwithLoggingcannot wrap.fsl logsreads production logs throughgcloud logging read, or.fsl-logs/with--local, with flags named after SQL clauses (--where,--select,--group-by,--order-by,--limit,--distinct,--since,--repeats). One JSON entry per line; a field or flag it does not know is an error that names the valid ones.fsl logs schemalists every label in the logs with counts and sample values (none foruserIdor keys containingemail,name,phone), and keeps labels your code writes with--add.fsl logs attachments <logId>downloads an entry's files.- The logs folder ignores itself in git. Every folder fsl creates for logs (
logLocalDir,.fsl-logs/) gets a.gitignoreof*unless one exists, so the README's.gitignorestep is gone.fsl logsnow says on stderr when 0 entries matched, andfsl logs schemakeeps samples for fsl's ownfunctionName. /fsl-logsskill for coding agents, installed byfsl install-skills./fsl-review [scope]skill andCAPABILITIES.md. The skill readsCAPABILITIES.mdfrom the installed package and the code in the scope (the app, a folder or a file), and proposes how fsl could best serve that code. It proposes and stops.CAPABILITIES.mdships in the package: for each capability, what it gives, the code signals it fits, how to add it, and the mistakes to avoid.fsl install-skillswritesfsl-version(the package version) into each skill's frontmatter, and asks before removing an installedlogsorquery-logs;--forceanswers yes.fsl doctorfindingskill-out-of-date(warning): a skill'sfsl-versiondiffers from the fsl running doctor. Skills withoutfsl-versionare skipped.BaseLabelsdeclares every label fsl writes, each with a one-line comment:repeatKey,repeatOf,repeatCount,firstSeen,lastSeen,sentLate,truncated,hasAttachments,logId,functionName,trace,run,slow,step,feedback.- Errors a framework catches are logged where it catches them.
createRoot(el, { onCaughtError: handleReactError })andapp.config.errorHandler = handleVueError(new) send React and Vue render errors,handleReactErrornow accepts what React 19 passes, and the React Router and Vue Router adapters log their router's errors on their own (errorTypeRouteError: a loader or action error or 5xx response asERROR, a 4xx response asWARNING; a guard that throws or a lazy route that fails to load asERROR). The function an adapter returns stops its error listener too. /testingcapture.settled()waits for every send already started, so a test can readentriesafter a log call with attachments or after a repeat summary; log methods still returnvoid.
Fixed
fsl doctormaps-publishedis an error only when nothing removes the maps before a deploy: nofsl upload-sourcemapsin apackage.jsonscript or hostingpredeploy, and no**/*.mapin hostingignore. A plainnpm run buildno longer fails doctor.fsl --help,-handhelp, and--helpor-hafter any command, print the usage and exit 0 instead of running the command (install-skills --helpran the install).fsl install-skillswith no terminal and no--forceno longer answers N for you: it asks nothing, installs what needs no answer, names on stderr what it left and the--forcethat does it, and exits 1./fsl-reviewandCAPABILITIES.mdno longer flag adata-fsl-viewvalue from a fixed set of code names, no longer raisemaxInstancesfrom code alone, and name both versions when a whole-app review finds two installs that differ.
Removed
/query-logsskill, which depended onfirebase-mcp-server.fsl install-skillsoffers to remove an installed copy./logsskill, replaced by/fsl-review: it carried facts in its own copy, which went stale.fsl install-skillsoffers to remove an installed copy.
v1.3.0
Where the user is, told by the router and the page. A router adapter takes route and
screen from React Router or Vue Router's own patterns and names, so a redirect on first
load is one breadcrumb and a :param route is one page. data-fsl-view marks name the
tabs, steps and dialogs on screen, and every entry carries them as a view label. And
/testing lets an app assert what it logs, with the real entries the logger sends.
Added
labelsForanddefaultLabelsForon/client/navigation: one function names a page'sroute,screenandpath, and its answer is used exactly as returned. It replacesrouteFor,cleanPathandpath: false.navigatedTo(screen, { route?, path? })for apps whose screens change without the URL changing — the same breadcrumb and labels as automatic navigation.bc.handledError— the clearer name for an error your code handled and did not log./testing:captureEntries()keeps every entry the logger sends, for your own tests, andresetSession()starts each test with an empty trail and a full budget.fsl doctorwarnslogs-inside-functions-sourcewhen local log files sit inside the Functions source folder.- Views:
enableViews()from/client/views, anddata-fsl-view="name"in your markup. Every browser entry carries aviewlabel naming the marked tabs, steps and dialogs visible when it was written, joined with›. - Router adapters:
enableVueRouterNavigation(router)from/client/navigation/vue-routerandenableReactRouterNavigation(router)from/client/navigation/react-router. They takerouteandscreenfrom the router's own patterns and names instead of guessing from the path. One option,adjust, edits the labels. Neither router is a dependency.
Changed
- One structured
navbreadcrumb per page change:nameis the screen (or the route),dataholdsrouteandpath. In 1.1–1.2 the name was the path. With navigation on,bc.navandsetScreenno longer add a second breadcrumb for the same change. - With navigation on, the
screenlabel comes from navigation, notsetScreen. An app that called both now sees navigation's screen on every entry, and repeat counting groups errors by that screen too.
Fixed
- React Router: a redirect on first load records one breadcrumb, for the page it ends on. It recorded the starting page too.
- The README's local log folder is now
.fsl-logs/at the repo root. Insidefunctions/, every entry restarted the emulator, which rotated the file and deleted entries within seconds.
Deprecated
enableNavigation({ routeFor }),({ cleanPath }),({ path: false })→labelsFor; therouteSourcelabel;bc.navandsetScreen→ navigation ornavigatedTo;bc.error→bc.handledError. Each warns once where code can, and is removed in 2.0 (#56).
v1.2.0
Timing traces. Some failures never throw: a loading screen waits on eight pieces, one of
them takes thirty seconds, and the logs say nothing. A trace reports exactly that — a
step or a whole flow that ran past its limit, or never finished — and nothing when it
ran in time. Your code names the steps; the limits live in one setup call.
Added
- Timing traces —
trace(name, fn)/startTrace(name)with.step(), from/client/timingand/functions, and oneconfigureTraces({...})holding every limit. A run that crosses a limit sends oneWARNING, while it is still running, with the steps done and still waiting; a run in time sends nothing. Browser runs that were hidden or paused (a laptop sleeping, a frozen tab) are not judged. Steps are standardperformance.measureentries underneath.
Fixed
npm publishno longer rewritesbin. It was declared as./dist/tools/index.js, which npm normalised with a warning on every publish.
v1.1.0
Navigation, automatically. One import, and every route change in a single-page app
becomes a breadcrumb, and every entry says which route and which real page it came
from. It is its own entry point, so an app that does not import it ships none of it —
the first of the optional helpers built that way.
Added
- Navigation, automatically —
enableNavigation()from the new/client/navigationentry point records every route change as a breadcrumb and labels each entry withroute(the pattern,/orders/:id/items),path(the real path) androuteSource.screenkeeps its meaning and falls back toroutewhen unset. An app that does not import it ships none of it; the query string and non-route fragments never leave the browser;routeFor,cleanPathandpath: falsefor apps that need them.
v1.0.0
The API is stable. From here, a breaking change means 2.0. Before freezing it, every
public name was reviewed and eight were renamed; the old ones keep working through 1.x
with a one-time warning, so upgrading breaks nothing. New in 1.0 is fsl doctor, which
reads a setup off disk and says what will go wrong before it does. Also here: warnings
can no longer spend the error reserve, one copy of Storage whatever firebase-admin a
project has, TypeScript 6, and Node 22 and 24 tested — Node 20 reached end of life.
Added
fsl doctorchecks the setup from disk — no network, no credentials — and prints how it will behave plus any findings, as text or--json. It reports only facts it can read from files, never guesses from source code. Errors fail with exit code 1,--strictfails on warnings too, and a check it could not run always fails.--backend/--distpoint it at a setup with nofirebase.json.
Fixed
- Warnings could spend the error reserve. The log limit recharges continuously, so its count is almost never a whole number, and the reserve check
available <= reservelet a warning through at, say, 1.02 with a reserve of 1 — spending the room kept for the next crash. It now asks whether spending one log would dip into the reserve. Found as a CI-only test flake: on a fast machine the calls share one millisecond and the count stays whole.
Changed
- Node 20 is no longer tested. It reached end of life on 2026-04-30.
enginesalready required Node 22, so nothing changes for a supported install; CI now runs Node 22 and 24, the current LTS. @google-cloud/storageis accepted as^7.19.0 || ^8.1.0(was^7.0.0). firebase-admin 14.5 brings Storage 8 and 13.x brings 7; with the old range a project on the newest firebase-admin installed two copies. Now npm reuses the one already there. Both majors are exercised bynpm run smoke:install, which CI now runs.- Built with TypeScript 6 (was 5.9), with
module/moduleResolution: node16. The published JavaScript is byte-identical and the.d.tsunchanged; a consumer on TypeScript 5.9, or resolving withnode16orbundler, compiles against it as before.
Deprecated
- Eight names, renamed before the 1.0 API freeze.
minLogLevel→minSeverity(the server already used it, and so does Cloud Logging);bucketName→bucket(every other bucket option already was);rateLimitOptions.sessionLimit→burstLimit,refillPerMinute→rechargeSecondsPerLog, anderrorReserve(a share) →reservedForErrors(a count), because the limit now recharges one log at a time rather than lasting a session; typeClientLogRequest→LogRequest;triggerTestLog()→sendTestLog(); the CLI flagfsl upload-sourcemaps --functions→--backend. Every old name still works in 1.x with a one-time console warning, and is removed in 2.0. The README's Reference has the full table.
v0.9.0
The logger no longer goes quiet when things get bad. A long session used to spend its
50-log budget by mid-morning and send nothing after, a flood of one error hid everything
behind it, and an entry past 100 KiB arrived as broken text. Now the budget refills and
keeps a fifth for errors, repeats are counted and sent as a summary — on the next visit
if the tab closed first — and a big entry is shortened with its full copy in Storage.
Each was checked live, and the live checks found two bugs the unit tests had passed.
Changed
- The client's log budget refills, keeps an error reserve, and turns repeats into summaries instead of dropping them (#40). The budget was 50 logs per page load, cleared on
beforeunload, with every severity spending from the same pool and every duplicate past 3 copies simply dropped. Now: the budget refills atrefillPerMinute(default 1/min) and survives a reload insessionStorage—beforeunloadno longer resets it; the lasterrorReserveshare (default 20%) can only be spent byERRORand above, by rank (SEVERITY_ORDER), soCRITICAL,ALERTandEMERGENCY— new severities, see below — may spend it too; and afterduplicateLimitfull copies, further occurrences are counted rather than dropped and sent as oneWARNINGsummary — hourly, when the tab is hidden, and once at startup for anything a previous visit left queued — carryingrepeatOf,repeatCount,firstSeen,lastSeen, andappId/releaseId/userId/screen/errorTyperecovered from the signature itself rather than a second stored copy of them. The browser has no serverlogIdfor the first copy, sorepeatOfinstead points at a client-sidelabels.repeatKeythat every full copy carries. The per-signature map is capped at 200 entries (evicting the oldest with nothing pending first) so a flood of distinct messages cannot grow it without bound. Unsent summaries wait inlocalStorage(survives the tab closing), capped atmaxPendingSummaries, expired aftersummaryMaxAgeDays, markedsentLate="true"when a later visit sends them, and removed from the queue only once their send has actually succeeded — a failed send (offline, or the tab closing right after) leaves it queued for the next flush or visit to retry, never twice. All seven numbers are configurable viarateLimitOptions. The three console messages a dropped or counted log produces changed to match. See README, "Volume controls". writeLog's production branch honours a client-suppliedtimestamp, but only on a repeat summary.LogPayloadgains an optionaltimestamp; it is written into the entry only whenlabels.repeatCountis present and the value is within the last 8 days and no more than 5 minutes in the future — so a summary is timestamped atlastSeen, not at send time, and anything else keeps Cloud Logging's own ingestion time.
Fixed
- An entry over 100 KiB arrived broken, with no severity or labels. Cloud Functions and Cloud Run both cut a stdout/stderr log line at exactly 102,400 bytes — measured live.
writeLog's production branch now measures the finished entry and, past 90 KiB, shortens it (breadcrumb data, then other context, then the tail of a long stack, then long text fields, then oversized label values —severity, labels, trace andserviceContextare never touched) before writing, saves the full original to Cloud Storage asfsl-overflow.jsonwhen a bucket is available, and labels the entrytruncated="true"(issue #21).
Documentation
/query-logscounts repeats. The shipped skill now addsrepeatCountfrom the summaries to the full copies when asked how often something happened, and readsfsl-overflow.jsonfor a shortened entry. The README's Querying table gains thetruncated,repeatKey/repeatOfandsentLatefilters.- The server half has to run on Google Cloud. 0.8.0's README said "any Node server". Entries go to stdout, which reaches Cloud Logging only on Cloud Functions, Cloud Run, GKE, App Engine, or Compute Engine with the Ops Agent — elsewhere they print and go nowhere.
v0.8.0
Firebase is optional now, and that is tested rather than claimed. 0.7.0 added
createHttpLogHandler for backends that are not Cloud Functions, but on one — Cloud
Run with no Firebase packages — /functions failed on require, and once loaded it
dropped every trace id. Both are fixed, and the smoke run now includes exactly that
service. Thanks to Vivek Rao, whose adoption found
the first of it.
Fixed
/functionscould not be loaded withoutfirebase-functionsinstalled, which madecreateHttpLogHandler(0.7.0) unusable on the backend it was written for.logger.tsimportedfirebase-functions/loggerat module load andlogHandler.tsimportedonCallthe same way, so a Cloud Run service without it failed onrequirebefore reaching the HTTP handler. Both are optional peers now, loaded lazily. Wherefirebase-functionsis installed nothing changes — itswrite()still attaches each trigger's trace id. Where it is not, entries are written as one JSON line each (WARNING and above to stderr, likewrite()), and the trace comes from the request headers. Reported and fixed by Vivek Rao in #39./functionscould not be loaded withoutfirebase-admineither.sourceMapCache.tsimportedfirebase-admin/storageat module load, for a bucket only older-release maps and attachments need. Storage now resolves on first use, down a chain:firebase-adminif installed (unchanged), else@google-cloud/storagewith the bucket you named, else no Storage — current-release embedded maps still resolve, older releases stay minified, attachments are dropped with the entry kept, and one warning says which.hasAttachmentsis no longer set on an entry whose attachments had nowhere to go. Found bynpm run smoke:install, a new check that installs the packed tarball with no optional peers.- Trace ids were never written on Cloud Run. 0.7.0 built
projects/<id>/traces/<trace>fromGCLOUD_PROJECTorGOOGLE_CLOUD_PROJECT, and Cloud Run sets neither — so behindcreateHttpLogHandlerthe trace was silently dropped. On Cloud Run the project now comes from the metadata server, once, the way Google's own Cloud Run sample does. Without a project the bare trace id is written, which is still a valid trace but does not meet Cloud Run's request log in "show entries for this trace"; the smoke run checks that the two do meet. - The client's production floor was never applied in a browser.
defaultMinLevelguarded ontypeof process, which is undefined in every browser even though bundlers foldprocess.env.NODE_ENVto a literal — so the folded branch was unreachable and every browser build defaulted toDEBUGin production. Seen in a real Vite bundle astypeof process<"u"?"WARNING":"DEBUG". The guard is gone; a runtime with neither the fold norprocessthrows on the read and is caught.
Documentation
- Without Firebase. The README says what is true now: the browser half has no Firebase dependency, and the server half runs on Cloud Functions, Cloud Run or any Node server. A new section covers what changes without Firebase —
authorize, naming a bucket, and which tools are Cloud Functions only. - A Hono adapter next to the Express one.
createHttpLogHandlertakes Node's request and response shapes, and Hono wraps them. - Volume controls: a Vite
define: { 'process.env': {} }makes the client's production floorDEBUG. PassminLogLevel.
v0.7.0 — errors group now
Errors group now. Not because this package learned to fingerprint them, but because Google
Cloud already runs an error tracker in your project and it could not see ours.
Added
-
Cloud Error Reporting shape. Production error entries carry
stack_traceat the top level and aserviceContextnaming yourappIdand release. Error Reporting reads Cloud Logging and groups by exception type plus the five top-most frames — with occurrence counts, an Open/Acknowledged/Resolved/Muted state, notifications, and an issue-tracker link — none of it ours to build or run, and none of it leaving your project.It groups on those top frames, which is why it does nothing for almost every web app: they read
app-4f2a.js:1:98432and change every release. We resolve them before the entry is written, so yours group. Verified live: two errors with different messages from one source location share a group id, attributed to theappIdrather than to the function that wrote them, stable across releases. Warnings and feedback are deliberately excluded — an issue is something a person has to resolve, and neither of those is a bug. -
createHttpLogHandler— receive client logs over plain HTTP, for a backend that is not Cloud Functions. Readsmethod,headersand a parsedbodyand writes throughstatusCode/setHeader/end, so Express, Fastify's compat layer and a raw server all satisfy it with no dependency added. The client was already portable:logFunctionis any(payload) => Promise<unknown>, andhttpsCallable()merely happens to fit.authorizeis required and has no default, because the honest default does not exist — a callable gets Firebase's token check for free, an HTTP endpoint gets nothing, and an open one writes to your Cloud Logging bill on anyone's say-so. Opening it deliberately is spelled'unauthenticated'at the call site. It is a gate, not an identity check: the handler never readsrequest.auth, and a gate that throws counts as a rejection. -
Trace ids outside Cloud Functions.
firebase-functionsattacheslogging.googleapis.com/tracefrom a store it populates inside its own request wrapper, so on Cloud Run nothing was attached and a request's entries did not correlate — silently. The HTTP handler now readsX-Cloud-Trace-Contextor W3Ctraceparentitself. A malformed header yields nothing rather than a guess: Cloud Logging rejects an invalid trace resource name, which costs the whole entry. -
Configurable Storage buckets and prefixes.
createClientLogFunction({ sourceMaps: { bucket, prefix } })is genuinely per-handler;configureAttachments({ bucket, prefix })is a separate global call, because the attachment upload happens inwriteLog— reached from every log call, including ones inside handlers that never touchcreateClientLogFunction— so a field on the handler would let a second one silently retarget the first's attachments.Every default is the previous behaviour, so nothing changes for a deploy that does not opt in. Worth opting in where user content needs its own region for residency, its own retention policy, or different IAM from your source maps — none of which a prefix can arrange.
-
fsl upload-sourcemapsruns without a bucket. With--embed-sourcemapsalone it embeds the current release and uploads nothing, which is the whole flow for a backend that has no bucket. Only the deployed release can then be symbolicated. Given neither a bucket nor an embed it now refuses outright rather than running, since that would delete the maps and produce nothing. -
fsl upload-sourcemaps --prefix, the writer half of the configurable Storage prefix.
Changed
Breaking: a production error's stack moved from jsonPayload.error.stack to top-level stack_trace. One copy, not two — the stack is the largest field in an entry capped at 256 KB. Anything reading error.stack off a production error entry reads stack_trace now. The rest of the error object — message, name, cause — is unchanged, and entries below ERROR, or marked as feedback, keep their stack where it was.
createClientLogHandler is no longer Firebase-shaped. It takes { data: LogPayload }, the minimum it reads, and throws ClientLogError rather than HttpsError. createClientLogFunction converts at the Firebase boundary, so a callable client sees exactly the error it saw before; only a caller wrapping the bare handler themselves is affected.
Every Storage path is defined once, in src/shared/paths.ts. Each source-map path previously existed twice — a writer in /tools, a reader in /functions — as independent literals with nothing enforcing that they matched, and when they disagree symbolication does not fail, it returns minified frames. Two things stopped being coincidences: the .map suffix, which the writer and reader agreed on only because a basename happened to end in it, and separators, since a Storage object name built with path.join is a different object on Windows.
A misconfigured source-map lookup says so. When nothing resolves — wrong bucket, wrong prefix, a release never uploaded, a missing fsl step — the function warns once per release naming both paths it tried. A prefix mismatch is only obvious once you can see the two strings.
Fixed
-
fslnever wrote the.releasemarker, so the release check had never run. The constant was declared, exported and used by nothing; the reader hardcoded its own copy. Every stack was symbolicated with the currently embedded maps whatever release it came from — precisely the failure the marker exists to prevent — and the fallback warning was unreachable code. Narrow in practice, since Vite content-hashes bundle filenames, but real for any build with stable names. The tests missed it because they wrote the marker themselves before exercising the reader. -
A prefix with a trailing slash addressed a different object.
'fsl/'yieldedfsl//r7/app.js.map, which is notfsl/r7/app.js.map— Storage keys are opaque strings and the double slash is not collapsed, so a slash typed out of habit would have split writer from reader silently.
Documentation
The README is reorganised around what you get rather than how the machine is built. How it works now precedes setup, so the Cloud Function is explained before you are asked to deploy one, and its diagram shows both doors into the stream. Setup is two paths, browser errors and Cloud Functions logging — backend setup previously did not exist as a path at all. New sections cover the eleven fields that arrive without being asked for, the five gates that silently drop a log, attachments as the way past the 256 KB entry limit, and querying. What this is not is rewritten, since it claimed there was no grouping.
skills/query-logs had severity=ERROR on its flagship query, which keeps the crash and throws away the sequence that led to it. The sequential read is now the headline example.
Internal
tests/ and smoke/ are typechecked for the first time, under a second config, and the smoke harness no longer uses any. Neither was checked before, which is how a read of entry.metadata.errorGroups — a field the Logging client does not surface — reached a live run and reported, wrongly, that Error Reporting had grouped nothing.
v0.6.0
Added
sendFeedback(text, extras?)— capture what error tracking structurally cannot see. A button that does nothing, a wrong total, the wrong data rendered: none of them throw, so none are logged. Feedback captures that class, and the breadcrumb trail already in memory turns "the discount didn't apply" from a complaint into a reproduction. Headless — the app owns the UI. Carries the same labels and breadcrumbs as any log, plus an optional screenshot.NOTICEseverity — ranks betweenINFOandWARNING, matching Cloud Logging's own ordering. Feedback from a person outranks routine status without being a warning about system health, and any existingseverity >= WARNINGalert ignores it with no configuration. This widens the publicLogSeverityunion, which is breaking for exhaustiveswitchstatements andRecord<LogSeverity, T>in consumer code.
Changed
- The 5-minute breadcrumb cutoff now applies when reading, not only when writing. It previously lapsed exactly when nothing was happening: a user who went idle for ten minutes and then hit an error shipped a trail of ten-minute-old steps presented as the path that led there.
- Every log now carries the full breadcrumb trail (up to 50 entries), not the newest 20. The trail already retained 50; the send path asked for 20, so 30 were kept in memory that nothing could read. Error and feedback payloads grow by roughly 3 KB, or ~2% of Cloud Logging's 256 KB entry limit.
Removed
Breaking: initRequestLogger is gone. Use withLogging. It bound the request scope with AsyncLocalStorage.enterWith(), which is never unwound, so a request's labels outlived the request — a later handler that did not call it inherited whichever user last touched the warm instance. Deprecated in 0.5.0 with a warning naming the leak. withLogging binds with run(), which restores the previous context when the handler settles, including on a throw.
Breaking: setActiveActivity and BaseLabels.activity are gone. activity was ambient state with no natural end — nothing cleared it automatically, so a forgotten one contaminated the rest of the session, and because it fed the dedup signature a stale value distorted which errors were suppressed. A wrong signal in a filtering rule is worse than a missing one. screen looks similar but is self-correcting: the next setScreen() overwrites it. Nothing set activity in practice, so removing it from the signature changes no behaviour — the segment was always empty. See #29 for restoring path discrimination from breadcrumbs, which are derived rather than manually maintained and therefore cannot go stale.
Fixed
- An unrecognised severity crashed
writeLogand destroyed the entry — both dispatch paths look the value up in a fixed table (CONSOLE_FNin emulator mode,firebase-functions' ownCONSOLE_SEVERITYinsideffWrite), and a miss resolves toundefined; calling it throws,Logger.send()'s catch swallows it, and the entry disappears with no diagnostic. It also slipped past the min-severity floor, sinceSEVERITY_ORDER[unknown]isundefinedandundefined > nisfalse.writeLogis exported, so a caller could reach it with a value read from config, crossing a type boundary, or from plain JavaScript. Unknown severities are now written asERRORwith a warning naming the bad value — an entry arriving loud beats one vanishing quietly.