@dasasian/firebase-structured-logger 0.8.0 → 0.9.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 +111 -15
- package/dist/client/logger.d.ts +16 -0
- package/dist/client/logger.d.ts.map +1 -1
- package/dist/client/logger.js +121 -16
- package/dist/client/logger.js.map +1 -1
- package/dist/client/rateLimiter.d.ts +98 -10
- package/dist/client/rateLimiter.d.ts.map +1 -1
- package/dist/client/rateLimiter.js +282 -18
- package/dist/client/rateLimiter.js.map +1 -1
- package/dist/functions/logHandler.d.ts.map +1 -1
- package/dist/functions/logHandler.js +1 -0
- 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/shared/types.d.ts +7 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/skills/query-logs/SKILL.md +35 -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,7 +66,7 @@ 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
|
|
|
@@ -198,7 +198,7 @@ The entry should carry `userId` without your having written it.
|
|
|
198
198
|
|
|
199
199
|
### If your backend is not Cloud Functions
|
|
200
200
|
|
|
201
|
-
Cloud Run, or any Node server you already run. Two things differ from the browser
|
|
201
|
+
Cloud Run, or any Node server you already run on Google Cloud. Two things differ from the browser
|
|
202
202
|
path above; everything else — breadcrumbs, labels, symbolication, the free fields — is
|
|
203
203
|
identical, and the entries land in the same stream in the same shape.
|
|
204
204
|
|
|
@@ -279,6 +279,9 @@ initLogger({
|
|
|
279
279
|
|
|
280
280
|
Nothing here needs a Firebase project — only a Google Cloud one. What changes:
|
|
281
281
|
|
|
282
|
+
- **The server has to run on Google Cloud** — Cloud Run, GKE, App Engine, or Compute
|
|
283
|
+
Engine with the Ops Agent. Entries are written to stdout, and only there does stdout
|
|
284
|
+
reach Cloud Logging. On AWS, Vercel or your own box they print and go nowhere.
|
|
282
285
|
- **The browser** needs no `firebase` package. `logFunction` is the `fetch` above; send
|
|
283
286
|
whatever credential your app already uses.
|
|
284
287
|
- **`authorize`** checks your own session instead of a Firebase ID token — for example
|
|
@@ -439,6 +442,12 @@ logger.error(err, { orderId })
|
|
|
439
442
|
| resolved file and line | function | your source maps — `Checkout.tsx:42`, not `app-4f2a.js:1:98432` |
|
|
440
443
|
| trace context | function | request correlation in Cloud Logging |
|
|
441
444
|
|
|
445
|
+
**Repeats are counted, not thrown away.** The usual answer to an error that fires 200 times
|
|
446
|
+
is to send the first few and drop the rest. This one sends 3 in full, counts the other 197,
|
|
447
|
+
and tells you when it started, when it stopped, which release and which user. Close the tab
|
|
448
|
+
and the count waits on the device for the next visit. See
|
|
449
|
+
[Repeats are counted, not dropped](#repeats-are-counted-not-dropped).
|
|
450
|
+
|
|
442
451
|
Backend logs get the same treatment: `withLogging` attaches `functionName`, `userId` from
|
|
443
452
|
the verified `request.auth.uid`, and whatever else you bind.
|
|
444
453
|
|
|
@@ -622,15 +631,33 @@ or different IAM from your source maps — none of which can be arranged with a
|
|
|
622
631
|
Nothing expires them. Add a lifecycle rule on `logAttachments/` to delete after N days, or
|
|
623
632
|
they accumulate for the life of the project.
|
|
624
633
|
|
|
634
|
+
## Big entries
|
|
635
|
+
|
|
636
|
+
Cloud Functions and Cloud Run both cut a stdout/stderr log line at exactly **102,400 bytes**
|
|
637
|
+
(100 KiB) — measured live. Past that the entry does not arrive truncated-but-valid: it arrives
|
|
638
|
+
as broken plain text, with no `severity`, no labels, and nothing for Error Reporting to group.
|
|
639
|
+
|
|
640
|
+
The backend logger watches for this. An entry over 90 KiB is shortened before it is written —
|
|
641
|
+
breadcrumb data first, then other context, then the tail of a long stack (the top frames are
|
|
642
|
+
what Error Reporting groups on, so those survive longest), then long text fields, and label
|
|
643
|
+
values only as a last resort. `severity`, labels, the trace and `serviceContext` are never
|
|
644
|
+
touched.
|
|
645
|
+
|
|
646
|
+
The full, unshortened entry is saved to Cloud Storage as `fsl-overflow.json`, at the same
|
|
647
|
+
`logAttachments/{logId}/` path an ordinary attachment would use, whenever a bucket is
|
|
648
|
+
available — see [Attachments](#attachments). A shortened entry carries `labels.truncated="true"`,
|
|
649
|
+
and `labels.hasAttachments="true"` when the full entry was saved. With no Storage configured,
|
|
650
|
+
the entry is still shortened; the original is lost, and the process warns once, not per entry.
|
|
651
|
+
|
|
625
652
|
## Volume controls
|
|
626
653
|
|
|
627
|
-
|
|
628
|
-
|
|
654
|
+
Several gates decide whether a log is written. All have defaults, and the defaults
|
|
655
|
+
hold things back — so this is worth reading before you conclude something is broken.
|
|
629
656
|
|
|
630
657
|
| Gate | Default | Where |
|
|
631
658
|
|---|---|---|
|
|
632
|
-
|
|
|
633
|
-
|
|
|
659
|
+
| Log budget | **50 logs**, refilling **1 per minute**; the last **20%** for errors only | client, per browser tab |
|
|
660
|
+
| Duplicates | **3** full copies of the same error, then counted and sent as a summary | client, per browser tab |
|
|
634
661
|
| Client severity floor | `WARNING` in production, `DEBUG` in dev | client, `minLogLevel` |
|
|
635
662
|
| Server severity floor | `WARNING` in production, `DEBUG` in the emulator | function, `minSeverity` |
|
|
636
663
|
| Function concurrency | `maxInstances: 1` on `createClientLogFunction` | function |
|
|
@@ -641,14 +668,18 @@ initLogger({
|
|
|
641
668
|
releaseId,
|
|
642
669
|
logFunction,
|
|
643
670
|
minLogLevel: 'INFO',
|
|
644
|
-
rateLimitOptions: {
|
|
671
|
+
rateLimitOptions: {
|
|
672
|
+
sessionLimit: 50, // the budget: how many logs can go at once
|
|
673
|
+
refillPerMinute: 1, // how fast it comes back
|
|
674
|
+
errorReserve: 0.2, // share of the budget only ERROR and above may spend
|
|
675
|
+
duplicateLimit: 3, // full copies of one error before counting starts
|
|
676
|
+
summaryIntervalMinutes: 60,
|
|
677
|
+
summaryMaxAgeDays: 7, // how long an unsent summary waits on the device
|
|
678
|
+
maxPendingSummaries: 50,
|
|
679
|
+
},
|
|
645
680
|
})
|
|
646
681
|
```
|
|
647
682
|
|
|
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
683
|
The client's production default comes from `process.env.NODE_ENV`, which Vite replaces at
|
|
653
684
|
build time. A `define: { 'process.env': {} }` in `vite.config` — common, to quiet a library
|
|
654
685
|
that expects Node — replaces the whole object instead, `NODE_ENV` reads as undefined, and
|
|
@@ -656,13 +687,72 @@ the floor is silently `DEBUG` in production. If your config has that line, pass
|
|
|
656
687
|
`minLogLevel` explicitly. Stating it is the safe habit either way: it is the one default
|
|
657
688
|
here whose failure mode is a bill rather than a missing log.
|
|
658
689
|
|
|
690
|
+
### The budget refills
|
|
691
|
+
|
|
692
|
+
Each browser tab starts with 50 logs. Every log spends one, and one comes back each minute,
|
|
693
|
+
up to 50. A reload does not reset it — the tab keeps its budget in `sessionStorage`.
|
|
694
|
+
|
|
695
|
+
So a burst at start-up is fine, and a user who works in the app all afternoon is never
|
|
696
|
+
silenced for long. A bug that logs in a loop is still held to about 60 entries an hour per
|
|
697
|
+
user: 1,000 users stuck in such a loop for a working month stays around the 50 GiB of
|
|
698
|
+
Cloud Logging that each project gets free.
|
|
699
|
+
|
|
700
|
+
The last 20% of the budget is reserved: warnings can spend it down to 10, and only
|
|
701
|
+
`ERROR` and above can spend the rest. A noisy warning cannot use up the room a crash needs.
|
|
702
|
+
|
|
703
|
+
### Repeats are counted, not dropped
|
|
704
|
+
|
|
705
|
+
Two errors are the same when the **message and the screen both match**, so the same error
|
|
706
|
+
on two different screens is kept apart. The first 3 are sent in full, with their stack and
|
|
707
|
+
breadcrumbs. After that the browser only counts them, and sends one summary:
|
|
708
|
+
|
|
709
|
+
```
|
|
710
|
+
WARNING Repeated 197 more times: cart sync failed
|
|
711
|
+
labels.repeatOf="<repeatKey of the first copy>" labels.repeatCount="197"
|
|
712
|
+
labels.firstSeen="…" labels.lastSeen="…"
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
The same count and window are in the entry's body, under `context.repeat`, so a summary is
|
|
716
|
+
filed as JSON like every other entry and a search on `jsonPayload.message` finds it.
|
|
717
|
+
|
|
718
|
+
The browser never learns the server's `logId` for the first copy — `writeLog` assigns it
|
|
719
|
+
after the log has already left the tab. So each full copy instead carries its own
|
|
720
|
+
`labels.repeatKey`, a client-side id, and the summary's `repeatOf` points at that instead:
|
|
721
|
+
query `labels.repeatKey="…" OR labels.repeatOf="…"` to find a full copy and its summary
|
|
722
|
+
together.
|
|
723
|
+
|
|
724
|
+
A summary is sent once an hour, when the tab is hidden, and once at startup for anything
|
|
725
|
+
a previous visit left queued. It is keyed by the error, the `releaseId` and the `userId`,
|
|
726
|
+
so two releases or two people on one computer are never counted together. Its entry is
|
|
727
|
+
timestamped at `lastSeen`, not at the time it was sent.
|
|
728
|
+
|
|
729
|
+
The summary's own labels are `appId`, `releaseId`, `userId`, `screen` and `errorType` — from
|
|
730
|
+
the time the errors happened, not from whoever's tab eventually sends it — plus
|
|
731
|
+
`repeatOf`/`repeatCount`/`firstSeen`/`lastSeen`/`sentLate`. Everything else about the error —
|
|
732
|
+
browser, platform, breadcrumbs, the stack — is not repeated here; it is already on the full
|
|
733
|
+
copies `repeatOf` points back to.
|
|
734
|
+
|
|
735
|
+
Unsent summaries are kept in `localStorage`, so closing the tab does not lose them: the
|
|
736
|
+
next visit sends them, marked `labels.sentLate="true"`. A summary is only removed from that
|
|
737
|
+
queue once it has actually been sent — a failed attempt (offline, or the tab closing right
|
|
738
|
+
after it was queued) leaves it for the next flush or the next visit to retry, never twice.
|
|
739
|
+
At most 50 wait, and any older than 7 days are deleted rather than sent — they are error
|
|
740
|
+
messages, and may hold personal data.
|
|
741
|
+
|
|
742
|
+
A summary is a `WARNING` with no stack, so Cloud Error Reporting sees the 3 full copies and
|
|
743
|
+
not the summary. For the true count, add up `labels.repeatCount` in Cloud Logging.
|
|
744
|
+
|
|
745
|
+
Summaries do not spend the log budget. There is at most one per error, per release, per user,
|
|
746
|
+
per hour.
|
|
747
|
+
|
|
659
748
|
### What a dropped log looks like
|
|
660
749
|
|
|
661
750
|
The two rate limits say so in the browser console:
|
|
662
751
|
|
|
663
752
|
```
|
|
664
|
-
[fsl] Duplicate
|
|
665
|
-
[fsl]
|
|
753
|
+
[fsl] Duplicate counted for the next summary: TypeError: cannot read 'id' | checkout
|
|
754
|
+
[fsl] Log budget empty — next log in about a minute
|
|
755
|
+
[fsl] Log budget: only errors can use the reserve now
|
|
666
756
|
```
|
|
667
757
|
|
|
668
758
|
**The severity floors are silent.** Both of them — the client's `minLogLevel` and the
|
|
@@ -697,6 +787,9 @@ Narrow it when you need to:
|
|
|
697
787
|
| `labels.screen="checkout"` | one screen |
|
|
698
788
|
| `labels.feedback="true"` | user-reported issues |
|
|
699
789
|
| `labels.hasAttachments="true"` | entries with files in GCS |
|
|
790
|
+
| `labels.truncated="true"` | entries shortened to fit — the full copy is `fsl-overflow.json` |
|
|
791
|
+
| `labels.repeatKey="<key>" OR labels.repeatOf="<key>"` | one repeating error: its full copies and its summaries |
|
|
792
|
+
| `labels.sentLate="true"` | repeat summaries sent on a later visit |
|
|
700
793
|
|
|
701
794
|
Locally, the emulator's JSONL answers the same questions. Point
|
|
702
795
|
**[firebase-mcp-server](https://github.com/dasasian/firebase-mcp-server)** at either and ask
|
|
@@ -832,6 +925,9 @@ Some of what is already handled here, all of it learned the expensive way:
|
|
|
832
925
|
disagree over a typo.
|
|
833
926
|
- **Checking a rate limit and spending it as two calls double-counts**, quietly making a
|
|
834
927
|
configured budget of 50 a budget of 25.
|
|
928
|
+
- **A fixed budget goes silent in a long session.** 50 logs per page load is spent by
|
|
929
|
+
mid-morning in an app someone keeps open all day, and the crash at lunch is never sent.
|
|
930
|
+
A budget that refills, with a share kept for errors, does not have that shape.
|
|
835
931
|
- **An old stack naming a bundle that still exists** resolves against the current release's
|
|
836
932
|
map, giving line numbers that are confidently wrong — worse than none, because nothing
|
|
837
933
|
signals it.
|
package/dist/client/logger.d.ts
CHANGED
|
@@ -46,6 +46,22 @@ export declare class Logger<AppLabels extends Record<string, string | undefined>
|
|
|
46
46
|
*/
|
|
47
47
|
sendFeedback(text: string, extras?: FeedbackOptions<AppLabels>): void;
|
|
48
48
|
private send;
|
|
49
|
+
private readonly summariesInFlight;
|
|
50
|
+
/** Send whatever repeat summaries are due, from this visit or an earlier one. */
|
|
51
|
+
sendPendingSummaries(): void;
|
|
52
|
+
/**
|
|
53
|
+
* A repeat summary: a WARNING with no stack, timestamped at `lastSeen` (see
|
|
54
|
+
* README, "Repeats are counted, not dropped"). It still respects the
|
|
55
|
+
* severity floor — only the budget and the duplicate gate are skipped, via
|
|
56
|
+
* `send`'s `skipBudget`.
|
|
57
|
+
*
|
|
58
|
+
* It is queued in `localStorage` precisely so a failed send does not lose
|
|
59
|
+
* it — offline, or the tab tearing down right after the `visibilitychange`
|
|
60
|
+
* that triggered this. So it is only acknowledged (removed from the queue)
|
|
61
|
+
* once `send` reports the entry actually reached `logFunction`; otherwise
|
|
62
|
+
* the next flush, or the next visit, finds it still there and retries it.
|
|
63
|
+
*/
|
|
64
|
+
private sendRepeatSummary;
|
|
49
65
|
}
|
|
50
66
|
/**
|
|
51
67
|
* Send user feedback through the configured logger.
|
|
@@ -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;AAWxF,OAAO,
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/client/logger.ts"],"names":[],"mappings":"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"}
|
package/dist/client/logger.js
CHANGED
|
@@ -136,33 +136,51 @@ class Logger {
|
|
|
136
136
|
// same pipe — it is rare by nature and there is nothing to throttle. The
|
|
137
137
|
// exemption keys on the record being feedback, not on its severity, so a
|
|
138
138
|
// NOTICE emitted for anything else still behaves normally.
|
|
139
|
-
bypassVolumeControls = false
|
|
139
|
+
bypassVolumeControls = false,
|
|
140
|
+
// A repeat summary spends no budget and is never itself a duplicate — but
|
|
141
|
+
// it still respects the severity floor, unlike feedback above.
|
|
142
|
+
skipBudget = false, timestamp) {
|
|
140
143
|
if (!bypassVolumeControls && severity_1.SEVERITY_ORDER[severity] > this.minLevel)
|
|
141
|
-
return;
|
|
144
|
+
return false;
|
|
145
|
+
const allLabels = {
|
|
146
|
+
appId: this.config.appId,
|
|
147
|
+
releaseId: this.config.releaseId,
|
|
148
|
+
screen: (0, breadcrumbs_1.getCurrentScreen)(),
|
|
149
|
+
userId: this.userId,
|
|
150
|
+
platform: PLATFORM,
|
|
151
|
+
browser: BROWSER,
|
|
152
|
+
...this.userLabels,
|
|
153
|
+
...labels,
|
|
154
|
+
};
|
|
142
155
|
// One gate for every severity. Passing a signature opts this log into
|
|
143
156
|
// duplicate suppression; the check and the budget spend are one operation,
|
|
144
157
|
// so a log can neither be counted twice nor checked without being counted.
|
|
145
|
-
|
|
158
|
+
// Once a signature passes `duplicateLimit`, further occurrences are
|
|
159
|
+
// "counted", not refused outright — the rate limiter keeps a running
|
|
160
|
+
// count toward the next repeat summary.
|
|
161
|
+
const decision = bypassVolumeControls || skipBudget
|
|
162
|
+
? { allowed: true }
|
|
163
|
+
: (0, rateLimiter_1.allow)({ severity, signature, labels: allLabels });
|
|
146
164
|
if (!decision.allowed) {
|
|
147
165
|
if (decision.reason === 'duplicate') {
|
|
148
|
-
console.warn(`[fsl] Duplicate
|
|
166
|
+
console.warn(`[fsl] Duplicate counted for the next summary: ${(0, rateLimiter_1.describeSignature)(decision.signature ?? '')}`);
|
|
167
|
+
}
|
|
168
|
+
else if (decision.reason === 'reserve') {
|
|
169
|
+
console.warn('[fsl] Log budget: only errors can use the reserve now');
|
|
149
170
|
}
|
|
150
171
|
else {
|
|
151
|
-
console.warn('[fsl]
|
|
172
|
+
console.warn('[fsl] Log budget empty — next log in about a minute');
|
|
152
173
|
}
|
|
153
|
-
return;
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
// Ties this full copy to the summary its repeats will eventually become.
|
|
177
|
+
// The client has no server logId to put in the summary's `repeatOf` — see
|
|
178
|
+
// README, "Repeats are counted, not dropped" — so this client-side id is
|
|
179
|
+
// the join key instead, carried by every full copy of the signature.
|
|
180
|
+
if ('repeatKey' in decision && decision.repeatKey) {
|
|
181
|
+
allLabels.repeatKey = decision.repeatKey;
|
|
154
182
|
}
|
|
155
183
|
try {
|
|
156
|
-
const allLabels = {
|
|
157
|
-
appId: this.config.appId,
|
|
158
|
-
releaseId: this.config.releaseId,
|
|
159
|
-
screen: (0, breadcrumbs_1.getCurrentScreen)(),
|
|
160
|
-
userId: this.userId,
|
|
161
|
-
platform: PLATFORM,
|
|
162
|
-
browser: BROWSER,
|
|
163
|
-
...this.userLabels,
|
|
164
|
-
...labels,
|
|
165
|
-
};
|
|
166
184
|
// Each attachment is converted in its own try. A Blob or File can fail to
|
|
167
185
|
// read — a user picks a file from <input type="file">, moves or deletes it,
|
|
168
186
|
// then submits, and the browser raises NotReadableError. Previously one
|
|
@@ -200,13 +218,65 @@ class Logger {
|
|
|
200
218
|
error,
|
|
201
219
|
},
|
|
202
220
|
...(base64Attachments ? { attachments: base64Attachments } : {}),
|
|
221
|
+
...(timestamp ? { timestamp } : {}),
|
|
203
222
|
};
|
|
204
223
|
await this.config.logFunction(payload);
|
|
224
|
+
return true;
|
|
205
225
|
}
|
|
206
226
|
catch (err) {
|
|
207
227
|
console.error('[fsl] Failed to send log:', err instanceof Error ? err.message : err);
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Guards against sending the same queued summary twice when a flush is
|
|
232
|
+
// triggered again (interval, visibilitychange) before a previous attempt's
|
|
233
|
+
// `logFunction` call has resolved — `peekPendingSummaries` does not remove
|
|
234
|
+
// anything, so without this a slow or slow-to-fail send could be picked up
|
|
235
|
+
// more than once.
|
|
236
|
+
summariesInFlight = new Set();
|
|
237
|
+
/** Send whatever repeat summaries are due, from this visit or an earlier one. */
|
|
238
|
+
sendPendingSummaries() {
|
|
239
|
+
for (const summary of (0, rateLimiter_1.peekPendingSummaries)()) {
|
|
240
|
+
if (this.summariesInFlight.has(summary.id))
|
|
241
|
+
continue;
|
|
242
|
+
this.summariesInFlight.add(summary.id);
|
|
243
|
+
void this.sendRepeatSummary(summary).finally(() => this.summariesInFlight.delete(summary.id));
|
|
208
244
|
}
|
|
209
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* A repeat summary: a WARNING with no stack, timestamped at `lastSeen` (see
|
|
248
|
+
* README, "Repeats are counted, not dropped"). It still respects the
|
|
249
|
+
* severity floor — only the budget and the duplicate gate are skipped, via
|
|
250
|
+
* `send`'s `skipBudget`.
|
|
251
|
+
*
|
|
252
|
+
* It is queued in `localStorage` precisely so a failed send does not lose
|
|
253
|
+
* it — offline, or the tab tearing down right after the `visibilitychange`
|
|
254
|
+
* that triggered this. So it is only acknowledged (removed from the queue)
|
|
255
|
+
* once `send` reports the entry actually reached `logFunction`; otherwise
|
|
256
|
+
* the next flush, or the next visit, finds it still there and retries it.
|
|
257
|
+
*/
|
|
258
|
+
async sendRepeatSummary(summary) {
|
|
259
|
+
const labels = {
|
|
260
|
+
appId: this.config.appId,
|
|
261
|
+
releaseId: summary.releaseId ?? this.config.releaseId,
|
|
262
|
+
userId: summary.userId,
|
|
263
|
+
screen: summary.screen,
|
|
264
|
+
errorType: summary.errorType,
|
|
265
|
+
repeatOf: summary.repeatOf,
|
|
266
|
+
repeatCount: String(summary.repeatCount),
|
|
267
|
+
firstSeen: summary.firstSeen,
|
|
268
|
+
lastSeen: summary.lastSeen,
|
|
269
|
+
...(summary.sentLate ? { sentLate: 'true' } : {}),
|
|
270
|
+
};
|
|
271
|
+
const sent = await this.send(`Repeated ${summary.repeatCount} more times: ${summary.message}`, 'WARNING', labels,
|
|
272
|
+
// A body besides the message. With only `message` left once the timestamp
|
|
273
|
+
// is promoted, Cloud Logging files the entry as textPayload, and a search on
|
|
274
|
+
// jsonPayload.message — /query-logs, or anyone's saved query — misses every
|
|
275
|
+
// summary. The smoke run found it.
|
|
276
|
+
{ repeat: { count: summary.repeatCount, firstSeen: summary.firstSeen, lastSeen: summary.lastSeen } }, undefined, undefined, undefined, false, true, summary.lastSeen);
|
|
277
|
+
if (sent)
|
|
278
|
+
(0, rateLimiter_1.acknowledgeSummary)(summary.id);
|
|
279
|
+
}
|
|
210
280
|
}
|
|
211
281
|
exports.Logger = Logger;
|
|
212
282
|
/**
|
|
@@ -243,6 +313,13 @@ function triggerTestLog() {
|
|
|
243
313
|
let instance = null;
|
|
244
314
|
function initLogger(config) {
|
|
245
315
|
instance = new Logger(config);
|
|
316
|
+
// A previous visit's queued summaries (README, "the next visit sends
|
|
317
|
+
// them") would otherwise wait for the first `visibilitychange: hidden` or
|
|
318
|
+
// the first 60s interval tick — a short delay so init itself can finish
|
|
319
|
+
// first, not a wait for either of those.
|
|
320
|
+
if (typeof setTimeout !== 'undefined') {
|
|
321
|
+
setTimeout(() => instance?.sendPendingSummaries(), 0);
|
|
322
|
+
}
|
|
246
323
|
return instance;
|
|
247
324
|
}
|
|
248
325
|
function getClientLogger() {
|
|
@@ -250,4 +327,32 @@ function getClientLogger() {
|
|
|
250
327
|
throw new Error('[fsl] initLogger() not called');
|
|
251
328
|
return instance;
|
|
252
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* Repeat summaries are sent hourly and when the tab is hidden (README,
|
|
332
|
+
* "Repeats are counted, not dropped"). Neither is triggered by an ordinary
|
|
333
|
+
* log call — `send` does not check for due summaries itself, since doing so
|
|
334
|
+
* with frozen-for-testing time reliably makes an interval-based check look
|
|
335
|
+
* due immediately (see tests/rateLimiter.ts). So there are exactly three
|
|
336
|
+
* triggers for a flush: `initLogger` (a previous visit's queue, once, above),
|
|
337
|
+
* this `visibilitychange` listener (the tab going hidden), and the interval
|
|
338
|
+
* below (an app that never hides its tab and rarely reloads).
|
|
339
|
+
*
|
|
340
|
+
* Neither exists in the esbuild bundle test's sandbox (no `window`/`document`
|
|
341
|
+
* there — see tests/browserBundle.ts), so both are guarded.
|
|
342
|
+
*/
|
|
343
|
+
if (typeof document !== 'undefined') {
|
|
344
|
+
document.addEventListener('visibilitychange', () => {
|
|
345
|
+
if (document.visibilityState !== 'hidden')
|
|
346
|
+
return;
|
|
347
|
+
(0, rateLimiter_1.flushDueSummaries)(true);
|
|
348
|
+
instance?.sendPendingSummaries();
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
if (typeof window !== 'undefined') {
|
|
352
|
+
const interval = setInterval(() => {
|
|
353
|
+
(0, rateLimiter_1.flushDueSummaries)();
|
|
354
|
+
instance?.sendPendingSummaries();
|
|
355
|
+
}, 60_000);
|
|
356
|
+
interval.unref?.();
|
|
357
|
+
}
|
|
253
358
|
//# sourceMappingURL=logger.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/client/logger.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/client/logger.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,63 @@
|
|
|
1
|
+
import type { LogSeverity } from '../shared/types';
|
|
1
2
|
export interface RateLimitConfig {
|
|
3
|
+
/** The budget: how many logs can go out at once. Default 50. */
|
|
2
4
|
sessionLimit?: number;
|
|
5
|
+
/** How fast the budget comes back, per minute. Default 1. */
|
|
6
|
+
refillPerMinute?: number;
|
|
7
|
+
/** Share of `sessionLimit` only ERROR and above may spend. Default 0.2. */
|
|
8
|
+
errorReserve?: number;
|
|
9
|
+
/** Full copies of one error before it is only counted. Default 3. */
|
|
3
10
|
duplicateLimit?: number;
|
|
4
11
|
storageKey?: string;
|
|
12
|
+
/** How often a running count of repeats is sent as a summary. Default 60. */
|
|
13
|
+
summaryIntervalMinutes?: number;
|
|
14
|
+
/** How long an unsent summary is kept before it is deleted, not sent. Default 7. */
|
|
15
|
+
summaryMaxAgeDays?: number;
|
|
16
|
+
/** Most pending summaries kept at once; oldest are dropped first. Default 50. */
|
|
17
|
+
maxPendingSummaries?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A repeat count waiting to be sent as a WARNING summary. Queued in
|
|
21
|
+
* `localStorage` so it survives the tab closing — `sessionStorage` does not.
|
|
22
|
+
*
|
|
23
|
+
* `bootId` is what `sentLate` is built from: it is the module's own load-time
|
|
24
|
+
* id, not anything persisted. A summary queued during an earlier page load
|
|
25
|
+
* carries a different `bootId` than the one currently running, so popping it
|
|
26
|
+
* here reveals it as "from a previous visit" without needing a server
|
|
27
|
+
* round-trip or a wall-clock guess.
|
|
28
|
+
*
|
|
29
|
+
* `message`/`errorType`/`screen`/`releaseId`/`userId` are short fields
|
|
30
|
+
* rebuilt once from the signature's key when the summary is created (see
|
|
31
|
+
* `parseSignatureKey`) — not a full label copy. Everything else about the
|
|
32
|
+
* error (browser, platform, stack, breadcrumbs…) is already in Cloud Logging
|
|
33
|
+
* on the full copies `repeatOf` points back to.
|
|
34
|
+
*/
|
|
35
|
+
export interface PendingSummary {
|
|
36
|
+
id: string;
|
|
37
|
+
message: string;
|
|
38
|
+
errorType?: string;
|
|
39
|
+
screen?: string;
|
|
40
|
+
releaseId?: string;
|
|
41
|
+
userId?: string;
|
|
42
|
+
repeatOf: string;
|
|
43
|
+
repeatCount: number;
|
|
44
|
+
firstSeen: string;
|
|
45
|
+
lastSeen: string;
|
|
46
|
+
createdAt: number;
|
|
47
|
+
bootId: string;
|
|
48
|
+
}
|
|
49
|
+
export interface SentSummary {
|
|
50
|
+
id: string;
|
|
51
|
+
message: string;
|
|
52
|
+
errorType?: string;
|
|
53
|
+
screen?: string;
|
|
54
|
+
releaseId?: string;
|
|
55
|
+
userId?: string;
|
|
56
|
+
repeatOf: string;
|
|
57
|
+
repeatCount: number;
|
|
58
|
+
firstSeen: string;
|
|
59
|
+
lastSeen: string;
|
|
60
|
+
sentLate: boolean;
|
|
5
61
|
}
|
|
6
62
|
export declare function configureRateLimiter(options: RateLimitConfig): void;
|
|
7
63
|
/**
|
|
@@ -15,25 +71,57 @@ export declare function configureRateLimiter(options: RateLimitConfig): void;
|
|
|
15
71
|
* without the staleness of a span someone has to remember to clear.
|
|
16
72
|
*/
|
|
17
73
|
export declare function signatureFor(error: Error | string, screen?: string): string;
|
|
74
|
+
/** A signature as a person reads it, for the console: `TypeError: cannot read 'id' | checkout`. */
|
|
75
|
+
export declare function describeSignature(signature: string): string;
|
|
18
76
|
export type RateLimitDecision = {
|
|
19
77
|
allowed: true;
|
|
78
|
+
repeatKey?: string;
|
|
20
79
|
} | {
|
|
21
80
|
allowed: false;
|
|
22
|
-
reason: 'session-limit' | 'duplicate';
|
|
81
|
+
reason: 'session-limit' | 'reserve' | 'duplicate';
|
|
23
82
|
signature?: string;
|
|
24
83
|
};
|
|
84
|
+
export interface AllowOptions {
|
|
85
|
+
signature?: string;
|
|
86
|
+
severity: LogSeverity;
|
|
87
|
+
labels?: Record<string, string | undefined>;
|
|
88
|
+
}
|
|
89
|
+
export declare function allow(options: AllowOptions): RateLimitDecision;
|
|
25
90
|
/**
|
|
26
|
-
*
|
|
91
|
+
* Move any signature's accumulated repeats into the pending-summary queue —
|
|
92
|
+
* every one whose window (`summaryIntervalMinutes`) has elapsed, or every one
|
|
93
|
+
* with anything to send when `force` is true (the tab going hidden).
|
|
27
94
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
|
|
32
|
-
|
|
95
|
+
* Only the counting resets; `count` (full copies already sent) is untouched,
|
|
96
|
+
* so a signature stays in "counting" mode across summary windows rather than
|
|
97
|
+
* sending three more full copies before the next summary.
|
|
98
|
+
*/
|
|
99
|
+
export declare function flushDueSummaries(force?: boolean): void;
|
|
100
|
+
/**
|
|
101
|
+
* Look at every pending summary not past `summaryMaxAgeDays`, WITHOUT
|
|
102
|
+
* removing it from the queue. A summary may only leave the queue once its
|
|
103
|
+
* send has actually succeeded — see `acknowledgeSummary` — so a failed send
|
|
104
|
+
* (offline, or the tab tearing down right after the `visibilitychange` that
|
|
105
|
+
* triggered this) leaves it queued for the next flush or the next visit to
|
|
106
|
+
* retry, instead of losing it. `sentLate` is true for anything queued by an
|
|
107
|
+
* earlier page load (a different `bootId`): the case the README's "next
|
|
108
|
+
* visit sends them" describes.
|
|
33
109
|
*
|
|
34
|
-
*
|
|
35
|
-
|
|
110
|
+
* Expired entries ARE removed here — there is no send to wait on for those.
|
|
111
|
+
*/
|
|
112
|
+
export declare function peekPendingSummaries(): SentSummary[];
|
|
113
|
+
/**
|
|
114
|
+
* Remove one summary from the queue by id, once its send has resolved. A
|
|
115
|
+
* retry that calls this again for an id already gone is a no-op — it never
|
|
116
|
+
* re-adds anything, so it cannot create a second entry.
|
|
117
|
+
*/
|
|
118
|
+
export declare function acknowledgeSummary(id: string): void;
|
|
119
|
+
/**
|
|
120
|
+
* Test-only reset. There is no `beforeunload` wiring any more — the budget
|
|
121
|
+
* lives in `sessionStorage` on purpose, so a reload keeps it — but tests still
|
|
122
|
+
* need a way to start a clean session, and to simulate a genuinely new visit
|
|
123
|
+
* (a new tab gets a new `bootId` for free, since the module reloads with it;
|
|
124
|
+
* a test in the same process cannot, so this rotates it explicitly).
|
|
36
125
|
*/
|
|
37
|
-
export declare function allow(signature?: string): RateLimitDecision;
|
|
38
126
|
export declare function resetRateLimiter(): void;
|
|
39
127
|
//# sourceMappingURL=rateLimiter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rateLimiter.d.ts","sourceRoot":"","sources":["../../src/client/rateLimiter.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rateLimiter.d.ts","sourceRoot":"","sources":["../../src/client/rateLimiter.ts"],"names":[],"mappings":"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"}
|