@dasasian/firebase-structured-logger 0.5.0 → 0.7.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 +652 -91
- package/dist/client/breadcrumbs.d.ts +14 -2
- package/dist/client/breadcrumbs.d.ts.map +1 -1
- package/dist/client/breadcrumbs.js +39 -21
- package/dist/client/breadcrumbs.js.map +1 -1
- package/dist/client/index.d.ts +3 -3
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +2 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/logger.d.ts +29 -0
- package/dist/client/logger.d.ts.map +1 -1
- package/dist/client/logger.js +39 -5
- package/dist/client/logger.js.map +1 -1
- package/dist/client/rateLimiter.d.ts +8 -3
- package/dist/client/rateLimiter.d.ts.map +1 -1
- package/dist/client/rateLimiter.js +9 -4
- package/dist/client/rateLimiter.js.map +1 -1
- package/dist/functions/httpHandler.d.ts +89 -0
- package/dist/functions/httpHandler.d.ts.map +1 -0
- package/dist/functions/httpHandler.js +112 -0
- package/dist/functions/httpHandler.js.map +1 -0
- package/dist/functions/index.d.ts +6 -3
- package/dist/functions/index.d.ts.map +1 -1
- package/dist/functions/index.js +6 -2
- package/dist/functions/index.js.map +1 -1
- package/dist/functions/logHandler.d.ts +53 -2
- package/dist/functions/logHandler.d.ts.map +1 -1
- package/dist/functions/logHandler.js +38 -7
- package/dist/functions/logHandler.js.map +1 -1
- package/dist/functions/logger.d.ts.map +1 -1
- package/dist/functions/logger.js +95 -7
- package/dist/functions/logger.js.map +1 -1
- package/dist/functions/requestLogger.d.ts +2 -20
- package/dist/functions/requestLogger.d.ts.map +1 -1
- package/dist/functions/requestLogger.js +2 -39
- package/dist/functions/requestLogger.js.map +1 -1
- package/dist/functions/sourceMapCache.d.ts +27 -1
- package/dist/functions/sourceMapCache.d.ts.map +1 -1
- package/dist/functions/sourceMapCache.js +84 -9
- package/dist/functions/sourceMapCache.js.map +1 -1
- package/dist/functions/traceContext.d.ts +40 -0
- package/dist/functions/traceContext.d.ts.map +1 -0
- package/dist/functions/traceContext.js +93 -0
- package/dist/functions/traceContext.js.map +1 -0
- package/dist/shared/paths.d.ts +59 -0
- package/dist/shared/paths.d.ts.map +1 -0
- package/dist/shared/paths.js +126 -0
- package/dist/shared/paths.js.map +1 -0
- package/dist/shared/severity.d.ts +21 -0
- package/dist/shared/severity.d.ts.map +1 -1
- package/dist/shared/severity.js +31 -3
- package/dist/shared/severity.js.map +1 -1
- package/dist/shared/types.d.ts +1 -2
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/tools/index.js +13 -4
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/uploadSourceMaps.d.ts +24 -1
- package/dist/tools/uploadSourceMaps.d.ts.map +1 -1
- package/dist/tools/uploadSourceMaps.js +31 -4
- package/dist/tools/uploadSourceMaps.js.map +1 -1
- package/package.json +4 -4
- package/skills/logs/SKILL.md +32 -9
- package/skills/query-logs/SKILL.md +14 -1
package/README.md
CHANGED
|
@@ -13,14 +13,46 @@
|
|
|
13
13
|
|
|
14
14
|
# @dasasian/firebase-structured-logger
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Your web app crashed at `app-4f2a.js:1:98432`. This tells you it was `Checkout.tsx:42` — in your own Google Cloud project, next to your backend logs. Nothing leaves.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Ships a client logger, a Cloud Functions logger, and the `fsl` CLI.
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
## One query, both halves
|
|
21
|
+
|
|
22
|
+
Frontend and backend write to the same stream in the same shape, so one filter reads the whole story in order:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
labels.userId="<uid>"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
10:42:03.114 INFO screen=checkout click "Apply code"
|
|
30
|
+
10:42:03.118 INFO screen=checkout applying discount SAVE20
|
|
31
|
+
10:42:03.402 INFO fn=applyDiscount started
|
|
32
|
+
10:42:03.611 ERROR fn=applyDiscount coupon lookup failed: timeout
|
|
33
|
+
10:42:03.798 ERROR screen=checkout TypeError at Checkout.tsx:42:9
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Two of those lines came from a browser and three from a server. You never had to think about that, and you never had to correlate two systems by timestamp to see it.
|
|
37
|
+
|
|
38
|
+
**Every label in that query and those lines was attached automatically.** You write the message; the rest rides along. See [What you get for free](#what-you-get-for-free).
|
|
39
|
+
|
|
40
|
+
## How it works
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
Browser Your Cloud Functions Cloud Logging
|
|
44
|
+
┌────────────────┐ ┌──────────────────────┐ ┌───────────────┐
|
|
45
|
+
│ no credentials │─────────▶│ logFrontendEvent() │───────▶│ your project │
|
|
46
|
+
│ no source maps │ │ credentials + maps │ │ one stream │
|
|
47
|
+
└────────────────┘ ├──────────────────────┤ │ one query │
|
|
48
|
+
│ your own functions │───────▶│ │
|
|
49
|
+
│ withLogging() │ └───────────────┘
|
|
50
|
+
└──────────────────────┘
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Why there is a function in the middle.** A browser cannot write to Cloud Logging — it has no credentials, and you would never ship credentials to a browser. So the frontend needs a door, and `logFrontendEvent` is it. Because every frontend error passes through that door anyway, it is also the only place that can hold your source maps: the browser must not have them (`fsl` strips them from `dist/` so they are never published), and Cloud Logging cannot apply them. **Symbolication happens there because there is nowhere else it can happen.**
|
|
54
|
+
|
|
55
|
+
Both boxes in the middle are your own Cloud Functions, in the deploy you already run. Nothing new to stand up.
|
|
24
56
|
|
|
25
57
|
## Install
|
|
26
58
|
|
|
@@ -34,7 +66,19 @@ cd functions && npm install @dasasian/firebase-structured-logger
|
|
|
34
66
|
|
|
35
67
|
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).
|
|
36
68
|
|
|
37
|
-
##
|
|
69
|
+
## Setup
|
|
70
|
+
|
|
71
|
+
Most projects do both halves. They share one step — `initLogger` inside `functions/` — and
|
|
72
|
+
are otherwise independent.
|
|
73
|
+
|
|
74
|
+
**Prerequisites**
|
|
75
|
+
|
|
76
|
+
- A `functions/` directory (`firebase init functions`, TypeScript) referenced by `firebase.json`.
|
|
77
|
+
- For browser errors: **Firebase Storage enabled** (source maps are uploaded there) —
|
|
78
|
+
Firebase console → **Storage → Get started** — and a frontend build that emits source maps.
|
|
79
|
+
The `fsl` source-map tooling assumes **Vite**.
|
|
80
|
+
|
|
81
|
+
### Catching errors from your browser
|
|
38
82
|
|
|
39
83
|
**1. Initialize the client** — at your app entry (e.g. `src/main.tsx`), before any logging:
|
|
40
84
|
|
|
@@ -52,7 +96,10 @@ export const logger = initLogger({
|
|
|
52
96
|
setupGlobalErrorHandler() // capture uncaught errors + unhandled rejections
|
|
53
97
|
```
|
|
54
98
|
|
|
55
|
-
|
|
99
|
+
`logFunction` is just `(payload) => Promise<unknown>`. `httpsCallable()` happens to fit it —
|
|
100
|
+
anything else that fits will work too.
|
|
101
|
+
|
|
102
|
+
**2. Add the log function** — in `functions/src/index.ts`:
|
|
56
103
|
|
|
57
104
|
```ts
|
|
58
105
|
import { initLogger, createClientLogFunction } from '@dasasian/firebase-structured-logger/functions'
|
|
@@ -60,174 +107,607 @@ import { initLogger, createClientLogFunction } from '@dasasian/firebase-structur
|
|
|
60
107
|
initLogger({ appId: 'my-app' })
|
|
61
108
|
|
|
62
109
|
export const logFrontendEvent = createClientLogFunction({
|
|
63
|
-
bucketName: 'my-app.firebasestorage.app', //
|
|
110
|
+
bucketName: 'my-app.firebasestorage.app', // holds source maps AND attachments
|
|
64
111
|
})
|
|
65
112
|
```
|
|
66
113
|
|
|
67
|
-
|
|
114
|
+
One bucket serves both, under two default prefixes:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
gs://my-app.firebasestorage.app/sourcemaps/{releaseId}/{bundle}.js.map
|
|
118
|
+
gs://my-app.firebasestorage.app/logAttachments/{logId}/{name}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Both the bucket and the prefix can be changed per half — see [Source maps](#source-maps)
|
|
122
|
+
for `sourceMaps: { bucket, prefix }`, and [Attachments](#attachments) for
|
|
123
|
+
`configureAttachments({ bucket, prefix })`. Omit `bucketName` entirely and both fall back
|
|
124
|
+
to your project's default bucket.
|
|
125
|
+
|
|
126
|
+
**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`:
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
"deploy": "export VITE_RELEASE_ID=$(git rev-parse --short HEAD) && npm run build && npx fsl upload-sourcemaps --functions=./functions --embed-sourcemaps && firebase deploy"
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`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.
|
|
133
|
+
|
|
134
|
+
> **`VITE_RELEASE_ID`** ties a build to its source maps — the client tags every entry with it, and `upload-sourcemaps` stores maps under the matching path. Use the same value in both places (the deploy script above sets it once from the git SHA). Locally it defaults to `'dev'`, and no maps are uploaded — symbolication isn't needed in development.
|
|
135
|
+
|
|
136
|
+
**4. Verify it works** — prove the round trip before you trust it:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { triggerTestLog } from '@dasasian/firebase-structured-logger/client'
|
|
140
|
+
|
|
141
|
+
triggerTestLog() // wire to a dev-only button; sends one error, one warning, one info
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Look for `labels.errorType="fsl-verify"` — in Cloud Logging once deployed, or in `dev.jsonl`
|
|
145
|
+
if you are running the emulator (see [Local development](#local-development)). Three entries,
|
|
146
|
+
and the error's stack should name a source file rather than a minified bundle. If they are
|
|
147
|
+
not there, nothing else in this README will work either.
|
|
148
|
+
|
|
149
|
+
Then log, anywhere in the frontend:
|
|
68
150
|
|
|
69
151
|
```ts
|
|
70
152
|
logger.info('checkout started', { orderId })
|
|
71
153
|
logger.error(err, { screen: 'camera' }, context, { photo: blob }) // attachments optional
|
|
72
154
|
```
|
|
73
155
|
|
|
74
|
-
Debug logs are suppressed in production automatically.
|
|
156
|
+
Debug logs are suppressed in production automatically.
|
|
75
157
|
|
|
76
|
-
|
|
158
|
+
### Logging from your Cloud Functions
|
|
77
159
|
|
|
78
|
-
|
|
160
|
+
A complete use of this package on its own — no client, no bundles, no source maps.
|
|
79
161
|
|
|
162
|
+
**1. Initialize the logger** — in `functions/src/index.ts`, at module load:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { initLogger } from '@dasasian/firebase-structured-logger/functions'
|
|
166
|
+
|
|
167
|
+
initLogger({ appId: 'my-app' })
|
|
80
168
|
```
|
|
81
|
-
|
|
82
|
-
|
|
169
|
+
|
|
170
|
+
Already done if you set up the browser half — it is the same call.
|
|
171
|
+
|
|
172
|
+
**2. Wrap your handlers** — `withLogging` binds the request's labels for the life of the handler:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
import { withLogging, logInfo, logError } from '@dasasian/firebase-structured-logger/functions'
|
|
176
|
+
|
|
177
|
+
export const checkout = onCall(
|
|
178
|
+
withLogging({ functionName: 'checkout' }, async (request) => {
|
|
179
|
+
logInfo('started') // carries functionName and the caller's userId already
|
|
180
|
+
...
|
|
181
|
+
logError(err, { orderId }) // labels merge with the request's
|
|
182
|
+
}),
|
|
183
|
+
)
|
|
83
184
|
```
|
|
84
185
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
186
|
+
`userId` comes from the verified `request.auth.uid`, so you never pass it. The scope unwinds
|
|
187
|
+
when the handler settles — one request's labels can never appear on another's logs, even on a
|
|
188
|
+
warm instance.
|
|
88
189
|
|
|
89
|
-
|
|
190
|
+
**3. Verify it works** — call the function, then look for `labels.functionName="checkout"`.
|
|
191
|
+
The entry should carry `userId` without your having written it.
|
|
90
192
|
|
|
91
|
-
###
|
|
193
|
+
### If your backend is not Cloud Functions
|
|
92
194
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
195
|
+
Cloud Run, or any Node server you already run. Two things differ from the browser
|
|
196
|
+
path above; everything else — breadcrumbs, labels, symbolication, the free fields — is
|
|
197
|
+
identical, and the entries land in the same stream in the same shape.
|
|
198
|
+
|
|
199
|
+
**Receive the logs over HTTP** instead of exporting a callable:
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
import express from 'express'
|
|
203
|
+
import { getAuth } from 'firebase-admin/auth'
|
|
204
|
+
import { initLogger, createHttpLogHandler } from '@dasasian/firebase-structured-logger/functions'
|
|
205
|
+
|
|
206
|
+
initLogger({ appId: 'my-app' })
|
|
207
|
+
|
|
208
|
+
const app = express()
|
|
209
|
+
app.use(express.json({ limit: '10mb' })) // attachments ride in the body
|
|
210
|
+
|
|
211
|
+
app.post('/log', createHttpLogHandler({
|
|
212
|
+
bucketName: 'my-app.firebasestorage.app', // holds source maps AND attachments
|
|
213
|
+
authorize: async (req) => {
|
|
214
|
+
const header = String(req.headers.authorization ?? '')
|
|
215
|
+
if (!header.startsWith('Bearer ')) return false
|
|
216
|
+
try { await getAuth().verifyIdToken(header.slice(7)); return true } catch { return false }
|
|
217
|
+
},
|
|
218
|
+
}))
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Point the client at it.** `logFunction` is any async function, so a `fetch` works:
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
initLogger({
|
|
225
|
+
appId: 'my-app',
|
|
226
|
+
releaseId: import.meta.env.VITE_RELEASE_ID ?? 'dev',
|
|
227
|
+
logFunction: async (payload) => {
|
|
228
|
+
const res = await fetch('https://api.example.com/log', {
|
|
229
|
+
method: 'POST',
|
|
230
|
+
headers: {
|
|
231
|
+
'Content-Type': 'application/json',
|
|
232
|
+
Authorization: `Bearer ${await auth.currentUser?.getIdToken()}`,
|
|
233
|
+
},
|
|
234
|
+
body: JSON.stringify(payload),
|
|
235
|
+
})
|
|
236
|
+
if (!res.ok) throw new Error(`log rejected: ${res.status}`)
|
|
237
|
+
},
|
|
238
|
+
})
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
#### `authorize` is required, and that is deliberate
|
|
242
|
+
|
|
243
|
+
A callable gets Firebase's token check for free. An HTTP endpoint gets nothing, and an
|
|
244
|
+
open one writes to your Cloud Logging bill on anyone's say-so. There is no honest
|
|
245
|
+
default, so there isn't one.
|
|
246
|
+
|
|
247
|
+
It is a **gate, not an identity check**. The handler never reads `request.auth` — the
|
|
248
|
+
`userId` on a client entry is self-reported either way. Its job is keeping strangers out.
|
|
249
|
+
|
|
250
|
+
If something in front of it already did that work — a VPC, an API gateway, IAM — say so
|
|
251
|
+
at the call site:
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
createHttpLogHandler({ authorize: 'unauthenticated' })
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
A gate that throws counts as a rejection, not an opening.
|
|
258
|
+
|
|
259
|
+
#### What you need to know
|
|
260
|
+
|
|
261
|
+
- **Body parsing is yours.** Mount `express.json()` (or your framework's equivalent)
|
|
262
|
+
before the handler. Raise its limit if you send attachments.
|
|
263
|
+
- **CORS** defaults to `*`, matching `cors: true` on the callable. Pass `allowOrigin` to
|
|
264
|
+
name your origin — a browser cannot send cookies to a wildcard.
|
|
265
|
+
- **Trace correlation works**, and needs nothing from you. The handler reads
|
|
266
|
+
`X-Cloud-Trace-Context` or `traceparent` off the request, so a request's entries still
|
|
267
|
+
group in Cloud Logging.
|
|
268
|
+
- **No Storage bucket?** You do not need one. `fsl upload-sourcemaps --embed-sourcemaps`
|
|
269
|
+
without `--bucket` embeds the current release's maps into your deploy and uploads
|
|
270
|
+
nothing. The catch: only the **deployed** release can be symbolicated, because older
|
|
271
|
+
ones live in a bucket there isn't one of. Errors from a previous release come back
|
|
272
|
+
minified.
|
|
273
|
+
- **Response codes:** `204` written, `400` malformed payload, `401` gate refused, `405`
|
|
274
|
+
not a POST, `500` something else. The client treats a non-2xx as a throw.
|
|
275
|
+
|
|
276
|
+
## Local development
|
|
277
|
+
|
|
278
|
+
The Functions emulator writes the same entries to a local JSONL file instead of Cloud
|
|
279
|
+
Logging, so the whole loop — client, log function, symbolication path, labels — works
|
|
280
|
+
before you deploy anything.
|
|
281
|
+
|
|
282
|
+
Add a `serve` script to `functions/package.json`:
|
|
283
|
+
|
|
284
|
+
```json
|
|
285
|
+
"serve": "npm run build && firebase emulators:start --only functions"
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Then tell the logger where to write, in your functions entry point:
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
initLogger({ appId: 'my-app', logLocalDir: 'logs' })
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
`logLocalDir` is yours to choose — it is resolved against the emulator's working directory,
|
|
295
|
+
which is `functions/`. `functions/logs` is the convention used throughout this README, not a
|
|
296
|
+
requirement; anywhere writable works, including a path outside the project.
|
|
96
297
|
|
|
97
|
-
|
|
298
|
+
Point **[firebase-mcp-server](https://github.com/dasasian/firebase-mcp-server)** at that file
|
|
299
|
+
to query your dev logs from Claude exactly as you would query Cloud Logging.
|
|
300
|
+
|
|
301
|
+
### Keep the logs out of git
|
|
98
302
|
|
|
99
303
|
```
|
|
100
304
|
functions/logs/*.jsonl
|
|
101
305
|
```
|
|
102
306
|
|
|
103
|
-
|
|
307
|
+
Track the directory, not the files:
|
|
104
308
|
|
|
105
309
|
```bash
|
|
106
310
|
mkdir -p functions/logs && touch functions/logs/.gitkeep
|
|
107
311
|
```
|
|
108
312
|
|
|
109
|
-
###
|
|
313
|
+
### Rotation
|
|
110
314
|
|
|
111
|
-
|
|
315
|
+
Entries go to `{logLocalDir}/dev.jsonl`. Each emulator start rotates the current file to
|
|
316
|
+
`dev-{timestamp}.jsonl`, and rotation also happens when the record limit is hit mid-session.
|
|
112
317
|
|
|
113
|
-
|
|
114
|
-
|
|
318
|
+
| Config | Default | Description |
|
|
319
|
+
|---|---|---|
|
|
320
|
+
| `logLocalDir` | — | Directory for local log files |
|
|
321
|
+
| `logMaxRecordsPerFile` | 2000 | Records per file before rotation |
|
|
322
|
+
| `logMaxRotatedFiles` | 5 | Rotated files to keep |
|
|
323
|
+
|
|
324
|
+
## Grouping, without a second product
|
|
325
|
+
|
|
326
|
+
Google Cloud already runs an error tracker in your project. **Error Reporting** watches
|
|
327
|
+
Cloud Logging, collapses repeats into issues, and gives you occurrence counts, a
|
|
328
|
+
resolution state — Open, Acknowledged, Resolved, Muted — notifications on new errors, and a
|
|
329
|
+
field to link your own issue tracker. It costs nothing beyond the logs you are already
|
|
330
|
+
writing, and it never sees anything outside your project.
|
|
331
|
+
|
|
332
|
+
It groups by exception type plus the **five top-most stack frames**. Which is why, for
|
|
333
|
+
almost every web app, it does nothing at all: those frames read `app-4f2a.js:1:98432`, they
|
|
334
|
+
change every release, and no two crashes ever look alike.
|
|
335
|
+
|
|
336
|
+
**We resolve the frames before the entry is written.** So yours read `Checkout.tsx:42`, and
|
|
337
|
+
they group:
|
|
338
|
+
|
|
339
|
+
```
|
|
340
|
+
TypeError: cannot read 'id' of undefined ← "the discount broke"
|
|
341
|
+
TypeError: order is not iterable ← "checkout is stuck"
|
|
342
|
+
two reports, two messages,
|
|
343
|
+
one line of code, one issue
|
|
115
344
|
```
|
|
116
345
|
|
|
117
|
-
|
|
346
|
+
Errors at `ERROR` and above carry `stack_trace` and a `serviceContext` naming your `appId`
|
|
347
|
+
and release, which is all Error Reporting needs. Nothing to enable in this package, and
|
|
348
|
+
nothing to configure.
|
|
118
349
|
|
|
119
|
-
|
|
350
|
+
Warnings stay out of it, and so does user feedback — an issue is something a person has to
|
|
351
|
+
resolve, and neither of those is a bug.
|
|
352
|
+
|
|
353
|
+
> Verified end to end against a real project: two errors with different messages from one
|
|
354
|
+
> source location land in a single group, attributed to the `appId` rather than to the
|
|
355
|
+
> function that wrote them, with the same group id across releases.
|
|
356
|
+
|
|
357
|
+
## What you get for free
|
|
358
|
+
|
|
359
|
+
You write one label. Eleven fields land.
|
|
360
|
+
|
|
361
|
+
```ts
|
|
362
|
+
logger.error(err, { orderId })
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
| Field | Added by | Where it comes from |
|
|
366
|
+
|---|---|---|
|
|
367
|
+
| `appId`, `releaseId` | client | your `initLogger` config |
|
|
368
|
+
| `screen` | client | tracked as the user moves |
|
|
369
|
+
| `userId` | client | `setUser`, held for the session |
|
|
370
|
+
| `platform` | client | user agent — `ios` / `android` / `macos` / `web` |
|
|
371
|
+
| `browser` | client | user agent |
|
|
372
|
+
| `errorType` | client | the Error's own `name` |
|
|
373
|
+
| last 50 breadcrumbs | client | the trail of what the user did |
|
|
374
|
+
| `logId` | function | a ULID, unique per entry — locates attachments in GCS |
|
|
375
|
+
| `hasAttachments` | function | `"true"` when files were uploaded alongside |
|
|
376
|
+
| resolved file and line | function | your source maps — `Checkout.tsx:42`, not `app-4f2a.js:1:98432` |
|
|
377
|
+
| trace context | function | request correlation in Cloud Logging |
|
|
378
|
+
|
|
379
|
+
Backend logs get the same treatment: `withLogging` attaches `functionName`, `userId` from
|
|
380
|
+
the verified `request.auth.uid`, and whatever else you bind.
|
|
381
|
+
|
|
382
|
+
That is the "structured" in the name. Not that the entry is JSON — that it arrives already
|
|
383
|
+
carrying who, where, which release, and what led up to it.
|
|
384
|
+
|
|
385
|
+
**The rule behind it:**
|
|
386
|
+
|
|
387
|
+
> You never pass context to a log call. You declare it once, and it rides along.
|
|
388
|
+
|
|
389
|
+
On the client that scope is the **session**. On the backend it is the **request**. Same idea,
|
|
390
|
+
two clocks — and it is why `labels.userId="…"` returns both halves: the client attaches the
|
|
391
|
+
uid from `setUser`, the backend from `request.auth.uid`, same label name, no coordination.
|
|
120
392
|
|
|
121
|
-
|
|
393
|
+
Declaring context does not replace passing it. There are three scopes, and they merge:
|
|
122
394
|
|
|
123
395
|
```ts
|
|
124
|
-
|
|
396
|
+
initLogger({ appId: 'my-app' }) // every log, for the life of the app
|
|
397
|
+
logger.setUser(uid, { orgId }) // every log, until clearUser()
|
|
398
|
+
logger.error(err, { orderId }) // this log only
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
Innermost wins — a label passed at the call site overrides the same label from `setUser`.
|
|
402
|
+
The backend works the same way: `withLogging` binds the request's labels, and each
|
|
403
|
+
`logInfo(message, labels)` can add or override for that one line.
|
|
404
|
+
|
|
405
|
+
## Adding your own context
|
|
406
|
+
|
|
407
|
+
Define your labels once, in a file both the app and `functions/` import:
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
// src/shared/labels.ts
|
|
411
|
+
export interface MyAppLabels {
|
|
125
412
|
organizationId?: string
|
|
126
413
|
itemId?: string
|
|
127
414
|
// whatever domain entities are relevant
|
|
128
415
|
}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Then use the same type on both sides.
|
|
129
419
|
|
|
420
|
+
**Client** — scoped to the session:
|
|
421
|
+
|
|
422
|
+
```ts
|
|
130
423
|
export const logger = initLogger<MyAppLabels>({ /* … */ })
|
|
131
424
|
|
|
132
|
-
logger.setUser(uid, {
|
|
425
|
+
logger.setUser(uid, { organizationId }) // on sign in — rides every log until cleared
|
|
133
426
|
logger.clearUser() // on sign out
|
|
134
427
|
logger.setScreen('checkout') // on navigation
|
|
135
428
|
```
|
|
136
429
|
|
|
137
|
-
|
|
430
|
+
**Backend** — scoped to the request:
|
|
138
431
|
|
|
139
|
-
|
|
432
|
+
```ts
|
|
433
|
+
import { withLogging, logInfo } from '@dasasian/firebase-structured-logger/functions'
|
|
434
|
+
|
|
435
|
+
export const checkout = onCall(
|
|
436
|
+
withLogging<MyAppLabels>(
|
|
437
|
+
(request) => ({ functionName: 'checkout', labels: { organizationId: request.data.orgId } }),
|
|
438
|
+
async (request) => {
|
|
439
|
+
logInfo('started') // carries functionName, userId and organizationId already
|
|
440
|
+
},
|
|
441
|
+
),
|
|
442
|
+
)
|
|
443
|
+
```
|
|
140
444
|
|
|
141
|
-
|
|
142
|
-
|
|
445
|
+
The function form runs per call, so labels can be derived from the request. The static form
|
|
446
|
+
from [setup](#logging-from-your-cloud-functions) is the same thing without that.
|
|
447
|
+
|
|
448
|
+
## Breadcrumbs
|
|
449
|
+
|
|
450
|
+
```ts
|
|
451
|
+
import { bc } from '@dasasian/firebase-structured-logger/client'
|
|
452
|
+
|
|
453
|
+
bc.nav('Checkout') // also sets labels.screen
|
|
454
|
+
bc.action('apply_discount', { code: 'SAVE10' })
|
|
455
|
+
bc.state('total_recalculated', { total: 42.00 })
|
|
456
|
+
bc.error('ValidationError', { field: 'price' })
|
|
143
457
|
```
|
|
144
458
|
|
|
145
|
-
|
|
459
|
+
A stack trace tells you where the code broke. It cannot tell you what the person did to
|
|
460
|
+
get there, which is usually the part you need to reproduce it. Breadcrumbs are that trail:
|
|
461
|
+
a rolling record of the last steps, attached automatically to every error and every piece
|
|
462
|
+
of feedback, with no correlation work on your side.
|
|
463
|
+
|
|
464
|
+
Drop a `bc.action` before anything that can fail and a `bc.nav` on every screen change, and
|
|
465
|
+
`total is wrong` arrives as `navigate_Checkout · apply_discount · total_recalculated ·
|
|
466
|
+
tap_place_order`.
|
|
467
|
+
|
|
468
|
+
The trail is capped at **50 entries** and **5 minutes** — old enough to cover the steps that
|
|
469
|
+
led here, short enough that it stays the current attempt rather than the whole session, and
|
|
470
|
+
bounded so a long-lived tab cannot grow it without limit. It lives in memory only, so it
|
|
471
|
+
never touches storage and never leaves the device except attached to a log you send.
|
|
472
|
+
|
|
473
|
+
Breadcrumbs are session-global by design: one user, one path. `bc.nav()` also sets the
|
|
474
|
+
current screen, so `labels.screen` stays correct without a second call.
|
|
475
|
+
|
|
476
|
+
> Record the step, not the data. Breadcrumb `data` is written to your logs verbatim — keep
|
|
477
|
+
> PII, tokens and card numbers out of it, the same as you would for any label.
|
|
146
478
|
|
|
147
|
-
##
|
|
479
|
+
## User feedback
|
|
148
480
|
|
|
149
481
|
```ts
|
|
150
|
-
|
|
151
|
-
logger.info(message, labels?, context?, attachments?)
|
|
152
|
-
logger.warning(message, labels?, context?, attachments?)
|
|
153
|
-
logger.debug(message, labels?, context?, attachments?) // suppressed in production
|
|
482
|
+
import { sendFeedback } from '@dasasian/firebase-structured-logger/client'
|
|
154
483
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
logger.setScreen(screen)
|
|
158
|
-
logger.addBreadcrumb(type, name, data?)
|
|
484
|
+
sendFeedback("the discount didn't apply")
|
|
485
|
+
sendFeedback(text, { attachments: { screenshot }, labels: { orderId } })
|
|
159
486
|
```
|
|
160
487
|
|
|
161
|
-
|
|
488
|
+
Error tracking only sees things that **throw**. A button that does nothing, a total that
|
|
489
|
+
comes out wrong, the wrong data rendered — none of them throw, so none are captured, and
|
|
490
|
+
you hear about them weeks later. `sendFeedback` captures that class, and it is actionable
|
|
491
|
+
because the breadcrumb trail is already in memory when the user hits send: *"the discount
|
|
492
|
+
didn't apply"* is a complaint; the same sentence plus `nav→Checkout · apply_discount ·
|
|
493
|
+
total_recalculated · tap_place_order` is a reproduction.
|
|
494
|
+
|
|
495
|
+
It carries everything a log carries — breadcrumbs, `screen`, `userId`, `releaseId`,
|
|
496
|
+
`platform`, `browser`, and any labels seeded via `setUser`. A screenshot passed as an
|
|
497
|
+
attachment rides the same Cloud Storage path as any other, so it is not bounded by the
|
|
498
|
+
entry size limit — see [Attachments](#attachments).
|
|
499
|
+
|
|
500
|
+
Headless: the package renders nothing, so the UI is yours. It returns nothing either —
|
|
501
|
+
a reference number is meaningless to a user with no portal to check it against. Say thank
|
|
502
|
+
you and move on; if the app wants correlation, pass its own id as a label.
|
|
503
|
+
|
|
504
|
+
Entries are written at **`NOTICE`**, which ranks between `INFO` and `WARNING`: feedback
|
|
505
|
+
from a person outranks routine status, and is not a warning about system health. Find it
|
|
506
|
+
with the severity dropdown, or `labels.feedback="true"`. Any existing `severity >= WARNING`
|
|
507
|
+
alert ignores it with no configuration.
|
|
162
508
|
|
|
163
|
-
|
|
509
|
+
Feedback is exempt from the severity floor and the rate limiter — those are volume controls
|
|
510
|
+
for events the system emits, and someone hitting send twice is not a duplicate to throttle.
|
|
511
|
+
|
|
512
|
+
## Attachments
|
|
164
513
|
|
|
165
514
|
```ts
|
|
166
|
-
|
|
515
|
+
logger.error(err, { orderId }, context, { photo: blob, state: JSON.stringify(cart) })
|
|
516
|
+
```
|
|
167
517
|
|
|
168
|
-
|
|
518
|
+
Any log method takes a final `attachments` argument — `Record<string, Blob | File | string>`
|
|
519
|
+
on the client, `Record<string, string | Buffer>` on the backend.
|
|
520
|
+
|
|
521
|
+
**Attachments are how you send more than a log entry can hold.** A Cloud Logging entry is
|
|
522
|
+
capped at **256 KB**, and a big payload does not get truncated — the write fails. Attachments
|
|
523
|
+
never enter the entry: they are uploaded to Cloud Storage and stripped before the entry is
|
|
524
|
+
written, so a 5 MB screenshot costs the log line two labels. Use them for anything that would
|
|
525
|
+
otherwise blow the cap — screenshots, request bodies, a serialised store, a captured frame.
|
|
526
|
+
|
|
527
|
+
They land at:
|
|
528
|
+
|
|
529
|
+
```
|
|
530
|
+
gs://<bucket>/logAttachments/{logId}/{name}
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
`logId` is a ULID on the entry itself, so the log line tells you where its files are:
|
|
169
534
|
|
|
170
|
-
export const myFunction = onCall(
|
|
171
|
-
withLogging({ functionName: 'myFunction' }, async (request) => {
|
|
172
|
-
logInfo('started') // carries userId + functionName automatically
|
|
173
|
-
}),
|
|
174
|
-
)
|
|
175
535
|
```
|
|
536
|
+
labels.hasAttachments="true" # entries that have files
|
|
537
|
+
labels.logId="01J..." # the entry whose files you are looking for
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
By default they share the bucket passed to `createClientLogFunction({ bucketName })` — the
|
|
541
|
+
same one the source maps live in, falling back to the project's default bucket.
|
|
176
542
|
|
|
177
|
-
|
|
178
|
-
afterwards, so one request's `userId` can never appear on another's logs. Labels that
|
|
179
|
-
depend on the request are computed per call:
|
|
543
|
+
Send them somewhere else with `configureAttachments`, in your functions entry point:
|
|
180
544
|
|
|
181
545
|
```ts
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
)
|
|
546
|
+
import { configureAttachments } from '@dasasian/firebase-structured-logger/functions'
|
|
547
|
+
|
|
548
|
+
configureAttachments({ bucket: 'my-app-user-content', prefix: 'evidence' })
|
|
186
549
|
```
|
|
187
550
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
> warm instance. Handlers that all call it are unaffected; the hazard is the ones that don't.
|
|
551
|
+
Call it once, at module load. It is global on purpose and global in the API: the upload
|
|
552
|
+
happens on every log call, including ones inside your own handlers that never touch
|
|
553
|
+
`createClientLogFunction`, so there is no per-handler setting for it to read. Fields you
|
|
554
|
+
leave out keep today's behaviour, and never calling it changes nothing.
|
|
193
555
|
|
|
194
|
-
|
|
556
|
+
Worth doing when user content needs its own region for residency, its own retention policy,
|
|
557
|
+
or different IAM from your source maps — none of which can be arranged with a prefix.
|
|
195
558
|
|
|
196
|
-
|
|
559
|
+
Nothing expires them. Add a lifecycle rule on `logAttachments/` to delete after N days, or
|
|
560
|
+
they accumulate for the life of the project.
|
|
197
561
|
|
|
198
|
-
|
|
199
|
-
# Upload source maps to Cloud Storage and strip local .map files (run in deploy)
|
|
200
|
-
npx fsl upload-sourcemaps [--bucket=<name>] [--functions=<path>] [--embed-sourcemaps] [--release=<id>]
|
|
562
|
+
## Volume controls
|
|
201
563
|
|
|
202
|
-
|
|
203
|
-
|
|
564
|
+
Three separate gates decide whether a log is written. All have defaults, and the defaults
|
|
565
|
+
drop things — so this is worth reading before you conclude something is broken.
|
|
566
|
+
|
|
567
|
+
| Gate | Default | Where |
|
|
568
|
+
|---|---|---|
|
|
569
|
+
| Session limit | **50 logs**, then the client stops sending | client, per browser session |
|
|
570
|
+
| Duplicate limit | **3 copies** of the same error, then it stops | client, per browser session |
|
|
571
|
+
| Client severity floor | `WARNING` in production, `DEBUG` in dev | client, `minLogLevel` |
|
|
572
|
+
| Server severity floor | `WARNING` in production, `DEBUG` in the emulator | function, `minSeverity` |
|
|
573
|
+
| Function concurrency | `maxInstances: 1` on `createClientLogFunction` | function |
|
|
574
|
+
|
|
575
|
+
```ts
|
|
576
|
+
initLogger({
|
|
577
|
+
appId: 'my-app',
|
|
578
|
+
releaseId,
|
|
579
|
+
logFunction,
|
|
580
|
+
minLogLevel: 'INFO',
|
|
581
|
+
rateLimitOptions: { sessionLimit: 200, duplicateLimit: 5 },
|
|
582
|
+
})
|
|
204
583
|
```
|
|
205
584
|
|
|
206
|
-
|
|
207
|
-
|
|
585
|
+
Two errors count as duplicates when the **message and the screen both match**, so the same
|
|
586
|
+
error on two different screens is not collapsed into one. The budget lives in
|
|
587
|
+
`sessionStorage` and resets with the session.
|
|
208
588
|
|
|
209
|
-
|
|
589
|
+
### What a dropped log looks like
|
|
590
|
+
|
|
591
|
+
The two rate limits say so in the browser console:
|
|
592
|
+
|
|
593
|
+
```
|
|
594
|
+
[fsl] Duplicate suppressed: TypeError: cannot read 'id'|checkout
|
|
595
|
+
[fsl] Session log limit reached
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
**The severity floors are silent.** Both of them — the client's `minLogLevel` and the
|
|
599
|
+
function's `minSeverity` — simply return, with nothing written and nothing logged about it.
|
|
600
|
+
|
|
601
|
+
So if an entry never arrived and there is no `[fsl]` warning in the console, it was a floor,
|
|
602
|
+
not a limit. In production both default to `WARNING`, which drops `DEBUG`, `INFO` and
|
|
603
|
+
`NOTICE` on the way out of the browser *and* again on the way into Cloud Logging — an
|
|
604
|
+
`INFO` you expected to see has two places it can vanish.
|
|
605
|
+
|
|
606
|
+
`maxInstances: 1` is a deliberate cost guard on what is usually the busiest function in the
|
|
607
|
+
system. Raise it (`createClientLogFunction({ bucketName, maxInstances: 5 })`) if you are
|
|
608
|
+
dropping client logs under load — and watch your Cloud Logging bill when you do.
|
|
609
|
+
|
|
610
|
+
Feedback is exempt from every one of these. See [User feedback](#user-feedback).
|
|
611
|
+
|
|
612
|
+
## Querying
|
|
613
|
+
|
|
614
|
+
One filter, both halves, in time order:
|
|
210
615
|
|
|
211
|
-
```bash
|
|
212
|
-
npx fsl install-skills
|
|
213
616
|
```
|
|
617
|
+
labels.userId="<uid>"
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
Narrow it when you need to:
|
|
621
|
+
|
|
622
|
+
| Filter | Returns |
|
|
623
|
+
|---|---|
|
|
624
|
+
| `labels.platform:*` | client entries only |
|
|
625
|
+
| `labels.functionName:*` | server entries only |
|
|
626
|
+
| `labels.releaseId="<sha>"` | one build |
|
|
627
|
+
| `labels.screen="checkout"` | one screen |
|
|
628
|
+
| `labels.feedback="true"` | user-reported issues |
|
|
629
|
+
| `labels.hasAttachments="true"` | entries with files in GCS |
|
|
630
|
+
|
|
631
|
+
Locally, the emulator's JSONL answers the same questions. Point
|
|
632
|
+
**[firebase-mcp-server](https://github.com/dasasian/firebase-mcp-server)** at either and ask
|
|
633
|
+
Claude instead — `npx fsl install-skills` installs the two skills below.
|
|
214
634
|
|
|
215
635
|
| Skill | Description |
|
|
216
636
|
|-------|-------------|
|
|
217
|
-
| `/logs` | Validate logging in a file — error paths, labels, PII,
|
|
637
|
+
| `/logs` | Validate logging in a file — error paths, labels, PII, unwrapped handlers, breadcrumbs |
|
|
218
638
|
| `/query-logs` | Query Cloud Logging or local JSONL via [firebase-mcp-server](https://github.com/dasasian/firebase-mcp-server) |
|
|
219
639
|
|
|
220
|
-
|
|
640
|
+
> A stack trace is self-reported by the browser, and so is `userId` on client entries — the
|
|
641
|
+
> uid comes from the client's own labels, not from a verified token. Backend entries are
|
|
642
|
+
> different: `withLogging` reads `request.auth.uid`, which Firebase has verified. Fine for
|
|
643
|
+
> debugging either way; don't build an audit trail on the client half.
|
|
221
644
|
|
|
222
|
-
|
|
645
|
+
## Reference
|
|
223
646
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
647
|
+
### Client
|
|
648
|
+
|
|
649
|
+
```ts
|
|
650
|
+
logger.error(error, labels?, context?, attachments?) // attachments: Record<string, Blob | File | string>
|
|
651
|
+
logger.info(message, labels?, context?, attachments?)
|
|
652
|
+
logger.warning(message, labels?, context?, attachments?)
|
|
653
|
+
logger.debug(message, labels?, context?, attachments?) // suppressed in production
|
|
654
|
+
|
|
655
|
+
logger.setUser(uid, extraLabels?)
|
|
656
|
+
logger.clearUser()
|
|
657
|
+
logger.setScreen(screen)
|
|
658
|
+
logger.addBreadcrumb(type, name, data?)
|
|
659
|
+
```
|
|
229
660
|
|
|
230
|
-
|
|
661
|
+
Also exported: `initLogger`, `getClientLogger`, `setupGlobalErrorHandler`, `handleReactError`,
|
|
662
|
+
`sendFeedback`, `triggerTestLog`, `addBreadcrumb`, `bc`.
|
|
663
|
+
|
|
664
|
+
`Logger` is exported as a **type only** — the client logger is a session singleton, so
|
|
665
|
+
annotate with `Logger<MyAppLabels>` and construct with `initLogger()`. A second instance
|
|
666
|
+
would silently share breadcrumbs, screen and the rate-limit budget while looking independent.
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
### Functions
|
|
670
|
+
|
|
671
|
+
```ts
|
|
672
|
+
initLogger({ appId, logLocalDir?, minSeverity?, logMaxRecordsPerFile?, logMaxRotatedFiles? })
|
|
673
|
+
|
|
674
|
+
withLogging(options | (request) => options, handler)
|
|
675
|
+
getLogger() // the current request's writer, or an anonymous fallback
|
|
676
|
+
logError / logWarn / logInfo / logDebug (message, labels?, context?, attachments?)
|
|
677
|
+
|
|
678
|
+
configureAttachments({ bucket?, prefix? }) // once, at module load — see Attachments
|
|
679
|
+
|
|
680
|
+
// Receiving client logs. All three take the same source-map config:
|
|
681
|
+
// { bucketName?, sourceMaps?: { bucket?, prefix? } }
|
|
682
|
+
createClientLogFunction({ …, cors?, maxInstances? }) // a ready-to-export callable
|
|
683
|
+
createHttpLogHandler({ …, authorize, allowOrigin? }) // an (req, res) handler for Express etc.
|
|
684
|
+
createClientLogHandler({ … }) // the bare handler, wrap it yourself
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
Backend log methods also accept an optional `attachments` (`Record<string, string | Buffer>`).
|
|
688
|
+
|
|
689
|
+
`createClientLogHandler` takes `{ data: LogPayload }` — the minimum it reads — and throws
|
|
690
|
+
`ClientLogError` with a `code` of `'invalid-argument'` or `'internal'`. The two wrappers above
|
|
691
|
+
translate that: the callable into an `HttpsError`, the HTTP handler into a status code. Use the
|
|
692
|
+
bare handler only if you are wrapping it in something else yourself.
|
|
693
|
+
|
|
694
|
+
### CLI (`fsl`)
|
|
695
|
+
|
|
696
|
+
```bash
|
|
697
|
+
# Upload source maps to Cloud Storage and strip local .map files (run in deploy)
|
|
698
|
+
npx fsl upload-sourcemaps --functions=./functions --embed-sourcemaps
|
|
699
|
+
|
|
700
|
+
# Same, to a bucket and prefix of your choosing — tell the reader the same values
|
|
701
|
+
npx fsl upload-sourcemaps --functions=./functions --embed-sourcemaps --bucket=my-maps --prefix=fsl-maps
|
|
702
|
+
|
|
703
|
+
# No bucket at all: embed the current release, upload nothing
|
|
704
|
+
npx fsl upload-sourcemaps --functions=./backend --embed-sourcemaps
|
|
705
|
+
|
|
706
|
+
# Install the Claude Code skills into the current project (or --global, --force)
|
|
707
|
+
npx fsl install-skills
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
### Source maps
|
|
231
711
|
|
|
232
712
|
For symbolicated production traces, emit source maps in Vite and upload them at deploy:
|
|
233
713
|
|
|
@@ -242,7 +722,88 @@ build: {
|
|
|
242
722
|
}
|
|
243
723
|
```
|
|
244
724
|
|
|
245
|
-
Maps are stored at `gs://<bucket>/sourcemaps/{releaseId}/{filename}.map` and loaded by the
|
|
725
|
+
Maps are stored at `gs://<bucket>/sourcemaps/{releaseId}/{filename}.map` and loaded by whichever log handler you deployed — the callable or the HTTP one — during symbolication.
|
|
726
|
+
|
|
727
|
+
To use a different bucket or prefix, tell **both ends** — they are two halves of one contract
|
|
728
|
+
and nothing checks them against each other:
|
|
729
|
+
|
|
730
|
+
```bash
|
|
731
|
+
npx fsl upload-sourcemaps --bucket=my-maps --prefix=fsl-maps …
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
```ts
|
|
735
|
+
createClientLogFunction({ sourceMaps: { bucket: 'my-maps', prefix: 'fsl-maps' } })
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
If they disagree the maps are simply not found and stacks stay minified — which looks
|
|
739
|
+
identical to never having uploaded them. The function warns once per release when it
|
|
740
|
+
resolves nothing, naming the exact object it looked for, so the mismatch is visible in your
|
|
741
|
+
logs rather than inferred.
|
|
742
|
+
|
|
743
|
+
## This is a hard problem
|
|
744
|
+
|
|
745
|
+
Not a difficult one — the pieces are all small. A hard one, in that the ways it goes wrong
|
|
746
|
+
are invisible until they aren't, and each is discovered by watching production do something
|
|
747
|
+
strange rather than by reading a doc.
|
|
748
|
+
|
|
749
|
+
Some of what is already handled here, all of it learned the expensive way:
|
|
750
|
+
|
|
751
|
+
- **Entry labels have to be emitted under `logging.googleapis.com/labels`.** Anywhere else
|
|
752
|
+
and they land inside the payload, so `labels.appId="…"` matches nothing. The logs look
|
|
753
|
+
perfect and cannot be filtered. Only a deployed run reveals it.
|
|
754
|
+
- **`AsyncLocalStorage.enterWith()` never unwinds.** A request's `userId` outlives the
|
|
755
|
+
request, and the next handler on a warm instance inherits whichever user came before.
|
|
756
|
+
- **Source maps left in `dist/` are your source code, published.** Uploading them is the
|
|
757
|
+
easy half; keeping them off the web server is the half people forget.
|
|
758
|
+
- **An unrecognised severity throws inside the write**, the entry is lost with no
|
|
759
|
+
diagnostic, and it slips past the severity floor on the way there.
|
|
760
|
+
- **A trailing slash on a Storage prefix is a different object.** `fsl//r7/app.js.map` is
|
|
761
|
+
not `fsl/r7/app.js.map`, and nothing collapses it — so the writer and reader silently
|
|
762
|
+
disagree over a typo.
|
|
763
|
+
- **Checking a rate limit and spending it as two calls double-counts**, quietly making a
|
|
764
|
+
configured budget of 50 a budget of 25.
|
|
765
|
+
- **An old stack naming a bundle that still exists** resolves against the current release's
|
|
766
|
+
map, giving line numbers that are confidently wrong — worse than none, because nothing
|
|
767
|
+
signals it.
|
|
768
|
+
- **`@google-cloud/logging` does not surface `errorGroups`.** Read grouping through the
|
|
769
|
+
client library and you will conclude, wrongly, that nothing grouped.
|
|
770
|
+
|
|
771
|
+
Every one of those is fixed here, and each has a test that fails if it comes back. That is
|
|
772
|
+
the point: you would have found them one at a time, in production, over months.
|
|
773
|
+
|
|
774
|
+
The list is not finished. It grows every time this is run against something real, and the
|
|
775
|
+
honest pitch is not that this package is complete — it plainly isn't — but that someone is
|
|
776
|
+
still walking into these and fixing them. Code you wrote yourself is frozen the day you
|
|
777
|
+
write it.
|
|
778
|
+
|
|
779
|
+
Decide for yourself whether that is worth a dependency.
|
|
780
|
+
|
|
781
|
+
## What this is not
|
|
782
|
+
|
|
783
|
+
**A product of ours.** The grouping above is Google's Error Reporting, running in your
|
|
784
|
+
project — we make its input legible, we do not build or run it. If it changes, you are
|
|
785
|
+
downstream of that, the same as you already are for Cloud Logging.
|
|
786
|
+
|
|
787
|
+
**A triage tool with a console.** There is no assignment, no ownership, no dashboard of
|
|
788
|
+
ours. What exists is Google's console, plus whatever queries you write.
|
|
789
|
+
|
|
790
|
+
There are hosted error trackers that do all of that, and do it well. The trade is worth
|
|
791
|
+
stating plainly, because it is the whole reason to choose this instead.
|
|
792
|
+
|
|
793
|
+
**Nothing leaves your project.** Every entry, every breadcrumb, every screenshot stays in
|
|
794
|
+
the Google Cloud project you already own, under your own IAM and your own retention rules.
|
|
795
|
+
No third party receives it, no third party stores it, and there is no data-processing
|
|
796
|
+
agreement to negotiate because there is no processor.
|
|
797
|
+
|
|
798
|
+
That matters most where it usually matters: **breadcrumbs and attachments carry what a user
|
|
799
|
+
was actually doing**, and a screenshot of a checkout page is not something everyone is free
|
|
800
|
+
to hand to a vendor. If you are in health, finance, education, or anywhere a contract names
|
|
801
|
+
where data may live, that is not a preference — it is the decision.
|
|
802
|
+
|
|
803
|
+
What you give up is a polished product: no vendor UI, no onboarding flow, no support
|
|
804
|
+
contract, no assignment workflow. What you get is every frontend and backend event in one
|
|
805
|
+
stream you already own, in one query language, grouped by a console that came with the
|
|
806
|
+
project.
|
|
246
807
|
|
|
247
808
|
## License
|
|
248
809
|
|