@dasasian/firebase-structured-logger 0.8.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +234 -35
  2. package/dist/client/index.d.ts +1 -1
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +2 -1
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/client/logger.d.ts +22 -2
  7. package/dist/client/logger.d.ts.map +1 -1
  8. package/dist/client/logger.js +135 -21
  9. package/dist/client/logger.js.map +1 -1
  10. package/dist/client/rateLimiter.d.ts +106 -11
  11. package/dist/client/rateLimiter.d.ts.map +1 -1
  12. package/dist/client/rateLimiter.js +334 -21
  13. package/dist/client/rateLimiter.js.map +1 -1
  14. package/dist/functions/httpHandler.d.ts +1 -1
  15. package/dist/functions/httpHandler.js +1 -1
  16. package/dist/functions/index.d.ts +1 -1
  17. package/dist/functions/index.d.ts.map +1 -1
  18. package/dist/functions/logHandler.d.ts +10 -5
  19. package/dist/functions/logHandler.d.ts.map +1 -1
  20. package/dist/functions/logHandler.js +17 -5
  21. package/dist/functions/logHandler.js.map +1 -1
  22. package/dist/functions/logger.d.ts.map +1 -1
  23. package/dist/functions/logger.js +232 -10
  24. package/dist/functions/logger.js.map +1 -1
  25. package/dist/functions/sourceMapCache.js +1 -1
  26. package/dist/functions/sourceMapCache.js.map +1 -1
  27. package/dist/shared/deprecate.d.ts +4 -0
  28. package/dist/shared/deprecate.d.ts.map +1 -0
  29. package/dist/shared/deprecate.js +30 -0
  30. package/dist/shared/deprecate.js.map +1 -0
  31. package/dist/shared/nodeModules.d.ts +39 -0
  32. package/dist/shared/nodeModules.d.ts.map +1 -0
  33. package/dist/shared/nodeModules.js +120 -0
  34. package/dist/shared/nodeModules.js.map +1 -0
  35. package/dist/shared/types.d.ts +7 -0
  36. package/dist/shared/types.d.ts.map +1 -1
  37. package/dist/shared/versionRange.d.ts +17 -0
  38. package/dist/shared/versionRange.d.ts.map +1 -0
  39. package/dist/shared/versionRange.js +48 -0
  40. package/dist/shared/versionRange.js.map +1 -0
  41. package/dist/tools/doctor.d.ts +54 -0
  42. package/dist/tools/doctor.d.ts.map +1 -0
  43. package/dist/tools/doctor.js +381 -0
  44. package/dist/tools/doctor.js.map +1 -0
  45. package/dist/tools/index.js +34 -5
  46. package/dist/tools/index.js.map +1 -1
  47. package/dist/tools/uploadSourceMaps.d.ts +7 -0
  48. package/dist/tools/uploadSourceMaps.d.ts.map +1 -1
  49. package/dist/tools/uploadSourceMaps.js.map +1 -1
  50. package/package.json +11 -11
  51. package/skills/query-logs/SKILL.md +49 -0
package/README.md CHANGED
@@ -17,8 +17,8 @@ Your web app crashed at `app-4f2a.js:1:98432`. This tells you it was `Checkout.t
17
17
 
18
18
  Ships a browser logger, a server logger, and the `fsl` CLI. Built for Firebase, but
19
19
  Firebase is optional: the browser half has no Firebase dependency, and the server half
20
- runs on Cloud Functions, Cloud Run, or any Node server. What it needs is a Google Cloud
21
- project — that is where the logs live.
20
+ runs on Cloud Functions, Cloud Run, or any Node server on Google Cloud, where stdout goes
21
+ to Cloud Logging. What it needs is a Google Cloud project — that is where the logs live.
22
22
 
23
23
  ## One query, both halves
24
24
 
@@ -66,11 +66,23 @@ npm install @dasasian/firebase-structured-logger
66
66
  # Cloud Functions
67
67
  cd functions && npm install @dasasian/firebase-structured-logger
68
68
 
69
- # Cloud Run, or any Node server
69
+ # Cloud Run, or any Node server on Google Cloud
70
70
  npm install @dasasian/firebase-structured-logger
71
71
  ```
72
72
 
73
- Ships ESM with three entry points — `/client`, `/functions`, `/tools` — plus the `fsl` CLI. `firebase`, `firebase-admin`, and `firebase-functions` are optional peer dependencies (bring your own versions). None of them is needed to load the package; each only switches on the part that uses it — see [Without Firebase](#without-firebase).
73
+ Ships CommonJS with TypeScript types — works from `require` and from `import`, in Node and in any bundler — with three entry points, `/client`, `/functions` and `/tools`, plus the `fsl` CLI. `firebase`, `firebase-admin`, and `firebase-functions` are optional peer dependencies (bring your own versions). None of them is needed to load the package; each only switches on the part that uses it — see [Without Firebase](#without-firebase).
74
+
75
+ Installing next to **firebase-admin 13** in one command can leave two copies of
76
+ `@google-cloud/storage` (8 for this package, 7 for firebase-admin). Both work; run
77
+ `npm dedupe` once and npm keeps the one they share. firebase-admin 14.5 and later use
78
+ the same Storage as this package, so there is nothing to do.
79
+
80
+ ### Upgrading from 0.x
81
+
82
+ Nothing breaks. Eight names were renamed for the 1.0 API — `minLogLevel` became
83
+ `minSeverity`, `bucketName` became `bucket`, and so on — and each old name still works in
84
+ 1.x, with a one-time console warning naming its replacement. They are removed in 2.0.
85
+ The full list is in [Renamed in 1.0](#client). Run `npx fsl doctor` after upgrading.
74
86
 
75
87
  ## Setup
76
88
 
@@ -113,7 +125,7 @@ import { initLogger, createClientLogFunction } from '@dasasian/firebase-structur
113
125
  initLogger({ appId: 'my-app' })
114
126
 
115
127
  export const logFrontendEvent = createClientLogFunction({
116
- bucketName: 'my-app.firebasestorage.app', // holds source maps AND attachments
128
+ bucket: 'my-app.firebasestorage.app', // holds source maps AND attachments
117
129
  })
118
130
  ```
119
131
 
@@ -126,13 +138,13 @@ gs://my-app.firebasestorage.app/logAttachments/{logId}/{name}
126
138
 
127
139
  Both the bucket and the prefix can be changed per half — see [Source maps](#source-maps)
128
140
  for `sourceMaps: { bucket, prefix }`, and [Attachments](#attachments) for
129
- `configureAttachments({ bucket, prefix })`. Omit `bucketName` entirely and both fall back
141
+ `configureAttachments({ bucket, prefix })`. Omit `bucket` entirely and both fall back
130
142
  to your project's default bucket.
131
143
 
132
144
  **3. Wire the deploy script** — upload source maps and strip them from the hosting bundle as part of deploy. Merge into your root `package.json` scripts, keeping any existing flags like `--project`:
133
145
 
134
146
  ```json
135
- "deploy": "export VITE_RELEASE_ID=$(git rev-parse --short HEAD) && npm run build && npx fsl upload-sourcemaps --functions=./functions --embed-sourcemaps && firebase deploy"
147
+ "deploy": "export VITE_RELEASE_ID=$(git rev-parse --short HEAD) && npm run build && npx fsl upload-sourcemaps --backend=./functions --embed-sourcemaps && firebase deploy"
136
148
  ```
137
149
 
138
150
  `fsl upload-sourcemaps` reads the bucket from `VITE_FIREBASE_STORAGE_BUCKET` (or `FIREBASE_STORAGE_BUCKET`) after loading `.env.local`. It uploads source maps to Cloud Storage, embeds a copy in `functions/sourcemaps/current/` for fast lookup, and deletes them from `dist/` so they are **not** served to browsers.
@@ -142,9 +154,9 @@ to your project's default bucket.
142
154
  **4. Verify it works** — prove the round trip before you trust it:
143
155
 
144
156
  ```ts
145
- import { triggerTestLog } from '@dasasian/firebase-structured-logger/client'
157
+ import { sendTestLog } from '@dasasian/firebase-structured-logger/client'
146
158
 
147
- triggerTestLog() // wire to a dev-only button; sends one error, one warning, one info
159
+ sendTestLog() // wire to a dev-only button; sends one error, one warning, one info
148
160
  ```
149
161
 
150
162
  Look for `labels.errorType="fsl-verify"` — in Cloud Logging once deployed, or in `dev.jsonl`
@@ -198,7 +210,7 @@ The entry should carry `userId` without your having written it.
198
210
 
199
211
  ### If your backend is not Cloud Functions
200
212
 
201
- Cloud Run, or any Node server you already run. Two things differ from the browser
213
+ Cloud Run, or any Node server you already run on Google Cloud. Two things differ from the browser
202
214
  path above; everything else — breadcrumbs, labels, symbolication, the free fields — is
203
215
  identical, and the entries land in the same stream in the same shape.
204
216
 
@@ -215,7 +227,7 @@ const app = express()
215
227
  app.use(express.json({ limit: '10mb' })) // attachments ride in the body
216
228
 
217
229
  app.post('/log', createHttpLogHandler({
218
- bucketName: 'my-app.firebasestorage.app', // holds source maps AND attachments
230
+ bucket: 'my-app.firebasestorage.app', // holds source maps AND attachments
219
231
  authorize: async (req) => {
220
232
  const header = String(req.headers.authorization ?? '')
221
233
  if (!header.startsWith('Bearer ')) return false
@@ -279,11 +291,14 @@ initLogger({
279
291
 
280
292
  Nothing here needs a Firebase project — only a Google Cloud one. What changes:
281
293
 
294
+ - **The server has to run on Google Cloud** — Cloud Run, GKE, App Engine, or Compute
295
+ Engine with the Ops Agent. Entries are written to stdout, and only there does stdout
296
+ reach Cloud Logging. On AWS, Vercel or your own box they print and go nowhere.
282
297
  - **The browser** needs no `firebase` package. `logFunction` is the `fetch` above; send
283
298
  whatever credential your app already uses.
284
299
  - **`authorize`** checks your own session instead of a Firebase ID token — for example
285
300
  `authorize: (req) => sessions.isValid(req.headers.cookie)`.
286
- - **Storage** has no Firebase default bucket to fall back to. Name one with `bucketName`
301
+ - **Storage** has no Firebase default bucket to fall back to. Name one with `bucket`
287
302
  (it holds source maps and attachments), and give the service's account access to it.
288
303
  Or name none: embedded maps still resolve the current release, older releases stay
289
304
  minified, and attachments are dropped — the log says so once.
@@ -325,7 +340,7 @@ A gate that throws counts as a rejection, not an opening.
325
340
  the trace id is still written, but does not join the platform's request log.
326
341
  - **No `firebase-admin`? Also optional.** Storage is only needed for older releases'
327
342
  source maps and for attachments. With `firebase-admin` installed, its Storage and
328
- default bucket are used. Without it, name the bucket — `bucketName` on the handler, or
343
+ default bucket are used. Without it, name the bucket — `bucket` on the handler, or
329
344
  `configureAttachments({ bucket })` — and the service's own credentials are used.
330
345
  - **No Storage bucket?** You do not need one. `fsl upload-sourcemaps --embed-sourcemaps`
331
346
  without `--bucket` embeds the current release's maps into your deploy and uploads
@@ -336,6 +351,75 @@ A gate that throws counts as a rejection, not an opening.
336
351
  - **Response codes:** `204` written, `400` malformed payload, `401` gate refused, `405`
337
352
  not a POST, `500` something else. The client treats a non-2xx as a throw.
338
353
 
354
+ ## Check your setup
355
+
356
+ Most of what can go wrong here goes wrong silently: logs that never arrive, stacks that
357
+ never resolve, source code published next to the app. `fsl doctor` reads the project from
358
+ disk and says what it finds. It needs no network and no credentials, and it only reports
359
+ facts it can read from a file — it never guesses from your source code, so a finding is
360
+ never a false alarm.
361
+
362
+ ```bash
363
+ npx fsl doctor # Firebase: reads firebase.json
364
+ npx fsl doctor --backend=./server --dist=./dist # anything else: say where things are
365
+ ```
366
+
367
+ It prints a summary of how your setup will behave, then any findings:
368
+
369
+ ```
370
+ Setup: Firebase — functions in ./functions, web build in ./dist
371
+
372
+ Logging firebase-functions write()
373
+ Trace ids from each Cloud Functions trigger
374
+ Storage firebase-admin, default bucket
375
+ Callable createClientLogFunction available
376
+
377
+ ⚠ duplicate-storage 2 copies of @google-cloud/storage (8.2.0 at the top level, 7.22.0 inside firebase-admin)
378
+ Fix: npm dedupe
379
+ ```
380
+
381
+ | Id | Level | Means |
382
+ |---|---|---|
383
+ | `maps-published` | error | `.map` files are in the folder hosting serves — your source code is public |
384
+ | `node-version` | error | the backend's Node is below 22 |
385
+ | `callable-without-firebase-functions` | error | `firebase.json` has functions, but `firebase-functions` is not installed there |
386
+ | `could-not-check` | error | doctor could not read something it needed — including "run `npm install` first" |
387
+ | `duplicate-storage` | warning | two copies of `@google-cloud/storage`; fix with `npm dedupe` |
388
+ | `unsupported-peer` | warning | an installed peer (`firebase-admin`, `firebase-functions`, `firebase`) is outside the supported range |
389
+ | `embedded-maps-without-release` | warning | maps are embedded without a `.release` marker, so an older release can resolve against the wrong map |
390
+
391
+ **Exit code:** `0` when there are no errors, `1` when there is one. `--strict` also fails on
392
+ warnings, for teams that want a spotless setup; without it, a deliberate choice like
393
+ running with no Storage does not break CI. A check doctor could not run is always an
394
+ error, never a pass.
395
+
396
+ **`--json`** prints the same result for scripts and agents:
397
+
398
+ ```json
399
+ {
400
+ "setup": { "kind": "firebase", "backend": "./functions", "dist": "./dist",
401
+ "logging": "firebase-functions", "trace": "trigger", "storage": "firebase-admin",
402
+ "callable": true },
403
+ "findings": [
404
+ { "id": "duplicate-storage", "level": "warning",
405
+ "message": "2 copies of @google-cloud/storage (8.2.0 at the top level, 7.22.0 inside firebase-admin)",
406
+ "fix": "npm dedupe" }
407
+ ],
408
+ "exitCode": 0
409
+ }
410
+ ```
411
+
412
+ `setup.kind` is `firebase`, `node`, `browser-only` or `backend-only`; `logging` is
413
+ `firebase-functions`, `stdout` or `none`; `trace` is `trigger`, `header` or `none`; and
414
+ `storage` is `firebase-admin`, `google-cloud-storage` (which still needs a bucket named in
415
+ your code — doctor cannot see it) or `none`. The ids and fields are part of the 1.0 API:
416
+ scripts may depend on them.
417
+
418
+ What doctor does not check: anything set in your own code — the release id you pass to
419
+ `initLogger`, a bucket name passed to a handler. Its Storage line says so — "needs a bucket named in code (not checked)" — rather than
420
+ guessing. [`fsl verify`](https://github.com/dasasian/firebase-structured-logger/issues/25),
421
+ planned after 1.0, will check those by sending a real log.
422
+
339
423
  ## Local development
340
424
 
341
425
  The Functions emulator writes the same entries to a local JSONL file instead of Cloud
@@ -439,6 +523,12 @@ logger.error(err, { orderId })
439
523
  | resolved file and line | function | your source maps — `Checkout.tsx:42`, not `app-4f2a.js:1:98432` |
440
524
  | trace context | function | request correlation in Cloud Logging |
441
525
 
526
+ **Repeats are counted, not thrown away.** The usual answer to an error that fires 200 times
527
+ is to send the first few and drop the rest. This one sends 3 in full, counts the other 197,
528
+ and tells you when it started, when it stopped, which release and which user. Close the tab
529
+ and the count waits on the device for the next visit. See
530
+ [Repeats are counted, not dropped](#repeats-are-counted-not-dropped).
531
+
442
532
  Backend logs get the same treatment: `withLogging` attaches `functionName`, `userId` from
443
533
  the verified `request.auth.uid`, and whatever else you bind.
444
534
 
@@ -600,7 +690,7 @@ labels.hasAttachments="true" # entries that have files
600
690
  labels.logId="01J..." # the entry whose files you are looking for
601
691
  ```
602
692
 
603
- By default they share the bucket passed to `createClientLogFunction({ bucketName })` — the
693
+ By default they share the bucket passed to `createClientLogFunction({ bucket })` — the
604
694
  same one the source maps live in, falling back to the project's default bucket.
605
695
 
606
696
  Send them somewhere else with `configureAttachments`, in your functions entry point:
@@ -622,16 +712,34 @@ or different IAM from your source maps — none of which can be arranged with a
622
712
  Nothing expires them. Add a lifecycle rule on `logAttachments/` to delete after N days, or
623
713
  they accumulate for the life of the project.
624
714
 
715
+ ## Big entries
716
+
717
+ Cloud Functions and Cloud Run both cut a stdout/stderr log line at exactly **102,400 bytes**
718
+ (100 KiB) — measured live. Past that the entry does not arrive truncated-but-valid: it arrives
719
+ as broken plain text, with no `severity`, no labels, and nothing for Error Reporting to group.
720
+
721
+ The backend logger watches for this. An entry over 90 KiB is shortened before it is written —
722
+ breadcrumb data first, then other context, then the tail of a long stack (the top frames are
723
+ what Error Reporting groups on, so those survive longest), then long text fields, and label
724
+ values only as a last resort. `severity`, labels, the trace and `serviceContext` are never
725
+ touched.
726
+
727
+ The full, unshortened entry is saved to Cloud Storage as `fsl-overflow.json`, at the same
728
+ `logAttachments/{logId}/` path an ordinary attachment would use, whenever a bucket is
729
+ available — see [Attachments](#attachments). A shortened entry carries `labels.truncated="true"`,
730
+ and `labels.hasAttachments="true"` when the full entry was saved. With no Storage configured,
731
+ the entry is still shortened; the original is lost, and the process warns once, not per entry.
732
+
625
733
  ## Volume controls
626
734
 
627
- Three separate gates decide whether a log is written. All have defaults, and the defaults
628
- drop things — so this is worth reading before you conclude something is broken.
735
+ Several gates decide whether a log is written. All have defaults, and the defaults
736
+ hold things back — so this is worth reading before you conclude something is broken.
629
737
 
630
738
  | Gate | Default | Where |
631
739
  |---|---|---|
632
- | Session limit | **50 logs**, then the client stops sending | client, per browser session |
633
- | Duplicate limit | **3 copies** of the same error, then it stops | client, per browser session |
634
- | Client severity floor | `WARNING` in production, `DEBUG` in dev | client, `minLogLevel` |
740
+ | Log limit | bursts of up to **50 logs**, then **one log recharges every 60 s**; the last **10** for errors only | client, per browser tab |
741
+ | Duplicates | **3** full copies of the same error, then counted and sent as a summary | client, per browser tab |
742
+ | Client severity floor | `WARNING` in production, `DEBUG` in dev | client, `minSeverity` |
635
743
  | Server severity floor | `WARNING` in production, `DEBUG` in the emulator | function, `minSeverity` |
636
744
  | Function concurrency | `maxInstances: 1` on `createClientLogFunction` | function |
637
745
 
@@ -640,32 +748,98 @@ initLogger({
640
748
  appId: 'my-app',
641
749
  releaseId,
642
750
  logFunction,
643
- minLogLevel: 'INFO',
644
- rateLimitOptions: { sessionLimit: 200, duplicateLimit: 5 },
751
+ minSeverity: 'INFO',
752
+ rateLimitOptions: {
753
+ burstLimit: 50, // how many logs can go at once
754
+ rechargeSecondsPerLog: 60, // after a burst, one log recharges every 60 seconds
755
+ reservedForErrors: 10, // of the burst, how many only ERROR and above may use
756
+ duplicateLimit: 3, // full copies of one error before counting starts
757
+ summaryIntervalMinutes: 60,
758
+ summaryMaxAgeDays: 7, // how long an unsent summary waits on the device
759
+ maxPendingSummaries: 50,
760
+ },
645
761
  })
646
762
  ```
647
763
 
648
- Two errors count as duplicates when the **message and the screen both match**, so the same
649
- error on two different screens is not collapsed into one. The budget lives in
650
- `sessionStorage` and resets with the session.
651
-
652
764
  The client's production default comes from `process.env.NODE_ENV`, which Vite replaces at
653
765
  build time. A `define: { 'process.env': {} }` in `vite.config` — common, to quiet a library
654
766
  that expects Node — replaces the whole object instead, `NODE_ENV` reads as undefined, and
655
767
  the floor is silently `DEBUG` in production. If your config has that line, pass
656
- `minLogLevel` explicitly. Stating it is the safe habit either way: it is the one default
768
+ `minSeverity` explicitly. Stating it is the safe habit either way: it is the one default
657
769
  here whose failure mode is a bill rather than a missing log.
658
770
 
771
+ ### Bursts recharge
772
+
773
+ Each browser tab can send a burst of up to `burstLimit` logs (50). After that, one log
774
+ recharges every `rechargeSecondsPerLog` seconds (60) — one at a time, so a full burst takes
775
+ 50 minutes to come back. A reload does not reset it: the tab keeps its count in
776
+ `sessionStorage`.
777
+
778
+ So a burst at start-up is fine, and a user who works in the app all afternoon is never
779
+ silenced for long. A bug that logs in a loop is still held to about 60 entries an hour per
780
+ user: 1,000 users stuck in such a loop for a working month stays around the 50 GiB of
781
+ Cloud Logging that each project gets free.
782
+
783
+ The last `reservedForErrors` logs (10) are kept for errors: warnings can use the burst down
784
+ to 10, and only `ERROR` and above can use the rest. A value over half of `burstLimit` is
785
+ capped at half, with one warning, so warnings always have room. A noisy warning cannot use up the room a crash needs.
786
+
787
+ ### Repeats are counted, not dropped
788
+
789
+ Two errors are the same when the **message and the screen both match**, so the same error
790
+ on two different screens is kept apart. The first 3 are sent in full, with their stack and
791
+ breadcrumbs. After that the browser only counts them, and sends one summary:
792
+
793
+ ```
794
+ WARNING Repeated 197 more times: cart sync failed
795
+ labels.repeatOf="<repeatKey of the first copy>" labels.repeatCount="197"
796
+ labels.firstSeen="…" labels.lastSeen="…"
797
+ ```
798
+
799
+ The same count and window are in the entry's body, under `context.repeat`, so a summary is
800
+ filed as JSON like every other entry and a search on `jsonPayload.message` finds it.
801
+
802
+ The browser never learns the server's `logId` for the first copy — `writeLog` assigns it
803
+ after the log has already left the tab. So each full copy instead carries its own
804
+ `labels.repeatKey`, a client-side id, and the summary's `repeatOf` points at that instead:
805
+ query `labels.repeatKey="…" OR labels.repeatOf="…"` to find a full copy and its summary
806
+ together.
807
+
808
+ A summary is sent once an hour, when the tab is hidden, and once at startup for anything
809
+ a previous visit left queued. It is keyed by the error, the `releaseId` and the `userId`,
810
+ so two releases or two people on one computer are never counted together. Its entry is
811
+ timestamped at `lastSeen`, not at the time it was sent.
812
+
813
+ The summary's own labels are `appId`, `releaseId`, `userId`, `screen` and `errorType` — from
814
+ the time the errors happened, not from whoever's tab eventually sends it — plus
815
+ `repeatOf`/`repeatCount`/`firstSeen`/`lastSeen`/`sentLate`. Everything else about the error —
816
+ browser, platform, breadcrumbs, the stack — is not repeated here; it is already on the full
817
+ copies `repeatOf` points back to.
818
+
819
+ Unsent summaries are kept in `localStorage`, so closing the tab does not lose them: the
820
+ next visit sends them, marked `labels.sentLate="true"`. A summary is only removed from that
821
+ queue once it has actually been sent — a failed attempt (offline, or the tab closing right
822
+ after it was queued) leaves it for the next flush or the next visit to retry, never twice.
823
+ At most 50 wait, and any older than 7 days are deleted rather than sent — they are error
824
+ messages, and may hold personal data.
825
+
826
+ A summary is a `WARNING` with no stack, so Cloud Error Reporting sees the 3 full copies and
827
+ not the summary. For the true count, add up `labels.repeatCount` in Cloud Logging.
828
+
829
+ Summaries do not count against the burst. There is at most one per error, per release, per user,
830
+ per hour.
831
+
659
832
  ### What a dropped log looks like
660
833
 
661
834
  The two rate limits say so in the browser console:
662
835
 
663
836
  ```
664
- [fsl] Duplicate suppressed: TypeError: cannot read 'id'|checkout
665
- [fsl] Session log limit reached
837
+ [fsl] Duplicate counted for the next summary: TypeError: cannot read 'id' | checkout
838
+ [fsl] Log limit reached — recharging, next log in about a minute
839
+ [fsl] Log limit: only errors can use the reserved logs now
666
840
  ```
667
841
 
668
- **The severity floors are silent.** Both of them — the client's `minLogLevel` and the
842
+ **The severity floors are silent.** Both of them — the client's `minSeverity` and the
669
843
  function's `minSeverity` — simply return, with nothing written and nothing logged about it.
670
844
 
671
845
  So if an entry never arrived and there is no `[fsl]` warning in the console, it was a floor,
@@ -674,7 +848,7 @@ not a limit. In production both default to `WARNING`, which drops `DEBUG`, `INFO
674
848
  `INFO` you expected to see has two places it can vanish.
675
849
 
676
850
  `maxInstances: 1` is a deliberate cost guard on what is usually the busiest function in the
677
- system. Raise it (`createClientLogFunction({ bucketName, maxInstances: 5 })`) if you are
851
+ system. Raise it (`createClientLogFunction({ bucket, maxInstances: 5 })`) if you are
678
852
  dropping client logs under load — and watch your Cloud Logging bill when you do.
679
853
 
680
854
  Feedback is exempt from every one of these. See [User feedback](#user-feedback).
@@ -697,6 +871,9 @@ Narrow it when you need to:
697
871
  | `labels.screen="checkout"` | one screen |
698
872
  | `labels.feedback="true"` | user-reported issues |
699
873
  | `labels.hasAttachments="true"` | entries with files in GCS |
874
+ | `labels.truncated="true"` | entries shortened to fit — the full copy is `fsl-overflow.json` |
875
+ | `labels.repeatKey="<key>" OR labels.repeatOf="<key>"` | one repeating error: its full copies and its summaries |
876
+ | `labels.sentLate="true"` | repeat summaries sent on a later visit |
700
877
 
701
878
  Locally, the emulator's JSONL answers the same questions. Point
702
879
  **[firebase-mcp-server](https://github.com/dasasian/firebase-mcp-server)** at either and ask
@@ -729,7 +906,21 @@ logger.addBreadcrumb(type, name, data?)
729
906
  ```
730
907
 
731
908
  Also exported: `initLogger`, `getClientLogger`, `setupGlobalErrorHandler`, `handleReactError`,
732
- `sendFeedback`, `triggerTestLog`, `addBreadcrumb`, `bc`.
909
+ `sendFeedback`, `sendTestLog`, `addBreadcrumb`, `bc`.
910
+
911
+ **Renamed in 1.0.** The old names still work in 1.x, each with a one-time console warning,
912
+ and are removed in 2.0:
913
+
914
+ | Old | New |
915
+ |---|---|
916
+ | `minLogLevel` (client `initLogger`) | `minSeverity` |
917
+ | `bucketName` (`createClientLogHandler`, `createClientLogFunction`, `createHttpLogHandler`) | `bucket` |
918
+ | `rateLimitOptions.sessionLimit` | `rateLimitOptions.burstLimit` |
919
+ | `rateLimitOptions.refillPerMinute` | `rateLimitOptions.rechargeSecondsPerLog` (seconds, not a rate: `60 / refillPerMinute`) |
920
+ | `rateLimitOptions.errorReserve` (a share) | `rateLimitOptions.reservedForErrors` (a count: `errorReserve × burstLimit`) |
921
+ | type `ClientLogRequest` | type `LogRequest` |
922
+ | `triggerTestLog()` | `sendTestLog()` |
923
+ | `fsl upload-sourcemaps --functions=<dir>` | `--backend=<dir>` (it is not only for Cloud Functions) |
733
924
 
734
925
  `Logger` is exported as a **type only** — the client logger is a session singleton, so
735
926
  annotate with `Logger<MyAppLabels>` and construct with `initLogger()`. A second instance
@@ -748,7 +939,7 @@ logError / logWarn / logInfo / logDebug (message, labels?, context?, attachments
748
939
  configureAttachments({ bucket?, prefix? }) // once, at module load — see Attachments
749
940
 
750
941
  // Receiving client logs. All three take the same source-map config:
751
- // { bucketName?, sourceMaps?: { bucket?, prefix? } }
942
+ // { bucket?, sourceMaps?: { bucket?, prefix? } }
752
943
  createClientLogFunction({ …, cors?, maxInstances? }) // a ready-to-export callable
753
944
  createHttpLogHandler({ …, authorize, allowOrigin? }) // an (req, res) handler for Express etc.
754
945
  createClientLogHandler({ … }) // the bare handler, wrap it yourself
@@ -765,13 +956,18 @@ bare handler only if you are wrapping it in something else yourself.
765
956
 
766
957
  ```bash
767
958
  # Upload source maps to Cloud Storage and strip local .map files (run in deploy)
768
- npx fsl upload-sourcemaps --functions=./functions --embed-sourcemaps
959
+ npx fsl upload-sourcemaps --backend=./functions --embed-sourcemaps
769
960
 
770
961
  # Same, to a bucket and prefix of your choosing — tell the reader the same values
771
- npx fsl upload-sourcemaps --functions=./functions --embed-sourcemaps --bucket=my-maps --prefix=fsl-maps
962
+ npx fsl upload-sourcemaps --backend=./functions --embed-sourcemaps --bucket=my-maps --prefix=fsl-maps
772
963
 
773
964
  # No bucket at all: embed the current release, upload nothing
774
- npx fsl upload-sourcemaps --functions=./backend --embed-sourcemaps
965
+ npx fsl upload-sourcemaps --backend=./backend --embed-sourcemaps
966
+
967
+ # Check the setup from disk — no network, no credentials (see "Check your setup")
968
+ npx fsl doctor
969
+ npx fsl doctor --backend=./server --dist=./dist # no firebase.json to read
970
+ npx fsl doctor --strict --json # CI: fail on warnings too, machine-readable
775
971
 
776
972
  # Install the Claude Code skills into the current project (or --global, --force)
777
973
  npx fsl install-skills
@@ -832,6 +1028,9 @@ Some of what is already handled here, all of it learned the expensive way:
832
1028
  disagree over a typo.
833
1029
  - **Checking a rate limit and spending it as two calls double-counts**, quietly making a
834
1030
  configured budget of 50 a budget of 25.
1031
+ - **A fixed budget goes silent in a long session.** 50 logs per page load is spent by
1032
+ mid-morning in an app someone keeps open all day, and the crash at lunch is never sent.
1033
+ A budget that refills, with a share kept for errors, does not have that shape.
835
1034
  - **An old stack naming a bundle that still exists** resolves against the current release's
836
1035
  map, giving line numbers that are confidently wrong — worse than none, because nothing
837
1036
  signals it.
@@ -1,4 +1,4 @@
1
- export { initLogger, getClientLogger, sendFeedback, triggerTestLog } from './logger';
1
+ export { initLogger, getClientLogger, sendFeedback, sendTestLog, triggerTestLog } from './logger';
2
2
  export type { Logger, InitLoggerConfig, FeedbackOptions, RateLimitConfig } from './logger';
3
3
  export { setupGlobalErrorHandler, handleReactError } from './errorHandler';
4
4
  export { addBreadcrumb, bc } from './breadcrumbs';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAMpF,YAAY,EAAE,MAAM,EAAE,gBAAgB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAC1F,OAAO,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAC1E,OAAO,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,eAAe,CAAA;AACjD,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAMjG,YAAY,EAAE,MAAM,EAAE,gBAAgB,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAC1F,OAAO,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAC1E,OAAO,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,eAAe,CAAA;AACjD,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA"}
@@ -1,10 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.bc = exports.addBreadcrumb = exports.handleReactError = exports.setupGlobalErrorHandler = exports.triggerTestLog = exports.sendFeedback = exports.getClientLogger = exports.initLogger = void 0;
3
+ exports.bc = exports.addBreadcrumb = exports.handleReactError = exports.setupGlobalErrorHandler = exports.triggerTestLog = exports.sendTestLog = exports.sendFeedback = exports.getClientLogger = exports.initLogger = void 0;
4
4
  var logger_1 = require("./logger");
5
5
  Object.defineProperty(exports, "initLogger", { enumerable: true, get: function () { return logger_1.initLogger; } });
6
6
  Object.defineProperty(exports, "getClientLogger", { enumerable: true, get: function () { return logger_1.getClientLogger; } });
7
7
  Object.defineProperty(exports, "sendFeedback", { enumerable: true, get: function () { return logger_1.sendFeedback; } });
8
+ Object.defineProperty(exports, "sendTestLog", { enumerable: true, get: function () { return logger_1.sendTestLog; } });
8
9
  Object.defineProperty(exports, "triggerTestLog", { enumerable: true, get: function () { return logger_1.triggerTestLog; } });
9
10
  var errorHandler_1 = require("./errorHandler");
10
11
  Object.defineProperty(exports, "setupGlobalErrorHandler", { enumerable: true, get: function () { return errorHandler_1.setupGlobalErrorHandler; } });
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":";;;AAAA,mCAAoF;AAA3E,oGAAA,UAAU,OAAA;AAAE,yGAAA,eAAe,OAAA;AAAE,sGAAA,YAAY,OAAA;AAAE,wGAAA,cAAc,OAAA;AAOlE,+CAA0E;AAAjE,uHAAA,uBAAuB,OAAA;AAAE,gHAAA,gBAAgB,OAAA;AAClD,6CAAiD;AAAxC,4GAAA,aAAa,OAAA;AAAE,iGAAA,EAAE,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":";;;AAAA,mCAAiG;AAAxF,oGAAA,UAAU,OAAA;AAAE,yGAAA,eAAe,OAAA;AAAE,sGAAA,YAAY,OAAA;AAAE,qGAAA,WAAW,OAAA;AAAE,wGAAA,cAAc,OAAA;AAO/E,+CAA0E;AAAjE,uHAAA,uBAAuB,OAAA;AAAE,gHAAA,gBAAgB,OAAA;AAClD,6CAAiD;AAAxC,4GAAA,aAAa,OAAA;AAAE,iGAAA,EAAE,OAAA"}
@@ -12,8 +12,10 @@ export interface InitLoggerConfig<AppLabels extends Record<string, string | unde
12
12
  appId: string;
13
13
  releaseId: string;
14
14
  logFunction: LogCallable;
15
- minLogLevel?: LogSeverity;
15
+ minSeverity?: LogSeverity;
16
16
  rateLimitOptions?: RateLimitConfig;
17
+ /** @deprecated Use `minSeverity`. */
18
+ minLogLevel?: LogSeverity;
17
19
  }
18
20
  export declare class Logger<AppLabels extends Record<string, string | undefined> = Record<string, string | undefined>> {
19
21
  private readonly config;
@@ -46,6 +48,22 @@ export declare class Logger<AppLabels extends Record<string, string | undefined>
46
48
  */
47
49
  sendFeedback(text: string, extras?: FeedbackOptions<AppLabels>): void;
48
50
  private send;
51
+ private readonly summariesInFlight;
52
+ /** Send whatever repeat summaries are due, from this visit or an earlier one. */
53
+ sendPendingSummaries(): void;
54
+ /**
55
+ * A repeat summary: a WARNING with no stack, timestamped at `lastSeen` (see
56
+ * README, "Repeats are counted, not dropped"). It still respects the
57
+ * severity floor — only the budget and the duplicate gate are skipped, via
58
+ * `send`'s `skipBudget`.
59
+ *
60
+ * It is queued in `localStorage` precisely so a failed send does not lose
61
+ * it — offline, or the tab tearing down right after the `visibilitychange`
62
+ * that triggered this. So it is only acknowledged (removed from the queue)
63
+ * once `send` reports the entry actually reached `logFunction`; otherwise
64
+ * the next flush, or the next visit, finds it still there and retries it.
65
+ */
66
+ private sendRepeatSummary;
49
67
  }
50
68
  /**
51
69
  * Send user feedback through the configured logger.
@@ -55,7 +73,7 @@ export declare class Logger<AppLabels extends Record<string, string | undefined>
55
73
  */
56
74
  export declare function sendFeedback<AppLabels extends Record<string, string | undefined> = Record<string, string | undefined>>(text: string, extras?: FeedbackOptions<AppLabels>): void;
57
75
  /**
58
- * Trigger a test log entry to verify the logging pipeline is working end-to-end.
76
+ * Send a test log entry to verify the logging pipeline is working end-to-end.
59
77
  * Logs at all severities with errorType: 'fsl-verify'. Safe to call in dev only.
60
78
  *
61
79
  * After clicking, check:
@@ -63,6 +81,8 @@ export declare function sendFeedback<AppLabels extends Record<string, string | u
63
81
  * 2. Stack trace is symbolicated (points to source file, not minified bundle)
64
82
  * 3. MCP query: source: local, where: [{ field: "labels.errorType", operator: "==", value: "fsl-verify" }]
65
83
  */
84
+ export declare function sendTestLog(): void;
85
+ /** @deprecated Use `sendTestLog()`. */
66
86
  export declare function triggerTestLog(): void;
67
87
  export declare function initLogger<AppLabels extends Record<string, string | undefined> = Record<string, string | undefined>>(config: InitLoggerConfig<AppLabels>): Logger<AppLabels>;
68
88
  export declare function getClientLogger<AppLabels extends Record<string, string | undefined> = Record<string, string | undefined>>(): Logger<AppLabels>;
@@ -1 +1 @@
1
- {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/client/logger.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/client/logger.ts"],"names":[],"mappings":"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"}