@dasasian/firebase-structured-logger 0.6.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 +598 -102
- 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 +5 -2
- package/dist/functions/index.d.ts.map +1 -1
- package/dist/functions/index.js +6 -1
- 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 +54 -3
- package/dist/functions/logger.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/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 +3 -3
- 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,107 +107,345 @@ 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.
|
|
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'
|
|
79
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' })
|
|
96
207
|
|
|
97
|
-
|
|
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.
|
|
297
|
+
|
|
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.
|
|
120
381
|
|
|
121
|
-
|
|
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.
|
|
392
|
+
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
Add a `serve` script to `functions/package.json`:
|
|
140
|
-
|
|
141
|
-
```json
|
|
142
|
-
"serve": "npm run build && firebase emulators:start --only functions"
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
The emulator writes entries to `DEV_LOG_DIR` (e.g. `functions/logs/dev.jsonl`), so you can inspect logs without deploying — and point **[firebase-mcp-server](https://github.com/dasasian/firebase-mcp-server)** at that same file to query your dev logs from Claude.
|
|
146
|
-
|
|
147
|
-
## Client API
|
|
430
|
+
**Backend** — scoped to the request:
|
|
148
431
|
|
|
149
432
|
```ts
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
+
)
|
|
159
443
|
```
|
|
160
444
|
|
|
161
|
-
|
|
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.
|
|
162
447
|
|
|
163
|
-
|
|
448
|
+
## Breadcrumbs
|
|
164
449
|
|
|
165
450
|
```ts
|
|
166
451
|
import { bc } from '@dasasian/firebase-structured-logger/client'
|
|
@@ -191,7 +476,7 @@ current screen, so `labels.screen` stays correct without a second call.
|
|
|
191
476
|
> Record the step, not the data. Breadcrumb `data` is written to your logs verbatim — keep
|
|
192
477
|
> PII, tokens and card numbers out of it, the same as you would for any label.
|
|
193
478
|
|
|
194
|
-
|
|
479
|
+
## User feedback
|
|
195
480
|
|
|
196
481
|
```ts
|
|
197
482
|
import { sendFeedback } from '@dasasian/firebase-structured-logger/client'
|
|
@@ -208,7 +493,9 @@ didn't apply"* is a complaint; the same sentence plus `nav→Checkout · apply_d
|
|
|
208
493
|
total_recalculated · tap_place_order` is a reproduction.
|
|
209
494
|
|
|
210
495
|
It carries everything a log carries — breadcrumbs, `screen`, `userId`, `releaseId`,
|
|
211
|
-
`platform`, `browser`, and any labels seeded via `setUser`.
|
|
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).
|
|
212
499
|
|
|
213
500
|
Headless: the package renders nothing, so the UI is yours. It returns nothing either —
|
|
214
501
|
a reference number is meaningless to a user with no portal to check it against. Say thank
|
|
@@ -222,77 +509,205 @@ alert ignores it with no configuration.
|
|
|
222
509
|
Feedback is exempt from the severity floor and the rate limiter — those are volume controls
|
|
223
510
|
for events the system emits, and someone hitting send twice is not a duplicate to throttle.
|
|
224
511
|
|
|
225
|
-
##
|
|
512
|
+
## Attachments
|
|
226
513
|
|
|
227
514
|
```ts
|
|
228
|
-
|
|
515
|
+
logger.error(err, { orderId }, context, { photo: blob, state: JSON.stringify(cart) })
|
|
516
|
+
```
|
|
229
517
|
|
|
230
|
-
|
|
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.
|
|
231
520
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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}
|
|
237
531
|
```
|
|
238
532
|
|
|
239
|
-
`
|
|
240
|
-
|
|
241
|
-
|
|
533
|
+
`logId` is a ULID on the entry itself, so the log line tells you where its files are:
|
|
534
|
+
|
|
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.
|
|
542
|
+
|
|
543
|
+
Send them somewhere else with `configureAttachments`, in your functions entry point:
|
|
242
544
|
|
|
243
545
|
```ts
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
)
|
|
546
|
+
import { configureAttachments } from '@dasasian/firebase-structured-logger/functions'
|
|
547
|
+
|
|
548
|
+
configureAttachments({ bucket: 'my-app-user-content', prefix: 'evidence' })
|
|
248
549
|
```
|
|
249
550
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
> relying on `getLogger()`'s anonymous fallback) inherited whichever user last touched the
|
|
255
|
-
> warm instance. Codebases where every handler called it were unaffected; the hazard was
|
|
256
|
-
> the ones that didn't. `withLogging` uses `run()`, which restores the previous scope when
|
|
257
|
-
> the handler settles.
|
|
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.
|
|
258
555
|
|
|
259
|
-
|
|
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.
|
|
260
558
|
|
|
261
|
-
|
|
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.
|
|
262
561
|
|
|
263
|
-
|
|
264
|
-
# Upload source maps to Cloud Storage and strip local .map files (run in deploy)
|
|
265
|
-
npx fsl upload-sourcemaps [--bucket=<name>] [--functions=<path>] [--embed-sourcemaps] [--release=<id>]
|
|
562
|
+
## Volume controls
|
|
266
563
|
|
|
267
|
-
|
|
268
|
-
|
|
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
|
+
})
|
|
269
583
|
```
|
|
270
584
|
|
|
271
|
-
|
|
272
|
-
|
|
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.
|
|
273
588
|
|
|
274
|
-
|
|
589
|
+
### What a dropped log looks like
|
|
275
590
|
|
|
276
|
-
|
|
277
|
-
|
|
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
|
|
278
596
|
```
|
|
279
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:
|
|
615
|
+
|
|
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.
|
|
634
|
+
|
|
280
635
|
| Skill | Description |
|
|
281
636
|
|-------|-------------|
|
|
282
637
|
| `/logs` | Validate logging in a file — error paths, labels, PII, unwrapped handlers, breadcrumbs |
|
|
283
638
|
| `/query-logs` | Query Cloud Logging or local JSONL via [firebase-mcp-server](https://github.com/dasasian/firebase-mcp-server) |
|
|
284
639
|
|
|
285
|
-
|
|
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.
|
|
286
644
|
|
|
287
|
-
|
|
645
|
+
## Reference
|
|
288
646
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
+
```
|
|
660
|
+
|
|
661
|
+
Also exported: `initLogger`, `getClientLogger`, `setupGlobalErrorHandler`, `handleReactError`,
|
|
662
|
+
`sendFeedback`, `triggerTestLog`, `addBreadcrumb`, `bc`.
|
|
294
663
|
|
|
295
|
-
|
|
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
|
|
296
711
|
|
|
297
712
|
For symbolicated production traces, emit source maps in Vite and upload them at deploy:
|
|
298
713
|
|
|
@@ -307,7 +722,88 @@ build: {
|
|
|
307
722
|
}
|
|
308
723
|
```
|
|
309
724
|
|
|
310
|
-
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.
|
|
311
807
|
|
|
312
808
|
## License
|
|
313
809
|
|