@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.
- package/README.md +234 -35
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +2 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/logger.d.ts +22 -2
- package/dist/client/logger.d.ts.map +1 -1
- package/dist/client/logger.js +135 -21
- package/dist/client/logger.js.map +1 -1
- package/dist/client/rateLimiter.d.ts +106 -11
- package/dist/client/rateLimiter.d.ts.map +1 -1
- package/dist/client/rateLimiter.js +334 -21
- package/dist/client/rateLimiter.js.map +1 -1
- package/dist/functions/httpHandler.d.ts +1 -1
- package/dist/functions/httpHandler.js +1 -1
- package/dist/functions/index.d.ts +1 -1
- package/dist/functions/index.d.ts.map +1 -1
- package/dist/functions/logHandler.d.ts +10 -5
- package/dist/functions/logHandler.d.ts.map +1 -1
- package/dist/functions/logHandler.js +17 -5
- package/dist/functions/logHandler.js.map +1 -1
- package/dist/functions/logger.d.ts.map +1 -1
- package/dist/functions/logger.js +232 -10
- package/dist/functions/logger.js.map +1 -1
- package/dist/functions/sourceMapCache.js +1 -1
- package/dist/functions/sourceMapCache.js.map +1 -1
- package/dist/shared/deprecate.d.ts +4 -0
- package/dist/shared/deprecate.d.ts.map +1 -0
- package/dist/shared/deprecate.js +30 -0
- package/dist/shared/deprecate.js.map +1 -0
- package/dist/shared/nodeModules.d.ts +39 -0
- package/dist/shared/nodeModules.d.ts.map +1 -0
- package/dist/shared/nodeModules.js +120 -0
- package/dist/shared/nodeModules.js.map +1 -0
- package/dist/shared/types.d.ts +7 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/versionRange.d.ts +17 -0
- package/dist/shared/versionRange.d.ts.map +1 -0
- package/dist/shared/versionRange.js +48 -0
- package/dist/shared/versionRange.js.map +1 -0
- package/dist/tools/doctor.d.ts +54 -0
- package/dist/tools/doctor.d.ts.map +1 -0
- package/dist/tools/doctor.js +381 -0
- package/dist/tools/doctor.js.map +1 -0
- package/dist/tools/index.js +34 -5
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/uploadSourceMaps.d.ts +7 -0
- package/dist/tools/uploadSourceMaps.d.ts.map +1 -1
- package/dist/tools/uploadSourceMaps.js.map +1 -1
- package/package.json +11 -11
- 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
|
|
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
|
|
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
|
-
|
|
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 `
|
|
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 --
|
|
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 {
|
|
157
|
+
import { sendTestLog } from '@dasasian/firebase-structured-logger/client'
|
|
146
158
|
|
|
147
|
-
|
|
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
|
-
|
|
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 `
|
|
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 — `
|
|
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({
|
|
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
|
-
|
|
628
|
-
|
|
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
|
-
|
|
|
633
|
-
|
|
|
634
|
-
| Client severity floor | `WARNING` in production, `DEBUG` in dev | client, `
|
|
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
|
-
|
|
644
|
-
rateLimitOptions: {
|
|
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
|
-
`
|
|
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
|
|
665
|
-
[fsl]
|
|
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 `
|
|
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({
|
|
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`, `
|
|
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
|
-
// {
|
|
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 --
|
|
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 --
|
|
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 --
|
|
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.
|
package/dist/client/index.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/client/index.js
CHANGED
|
@@ -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; } });
|
package/dist/client/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":";;;AAAA,
|
|
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"}
|
package/dist/client/logger.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
*
|
|
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":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAgB,UAAU,EAAE,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/client/logger.ts"],"names":[],"mappings":"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"}
|