Migrating to 0.2.0
0.2.0 is the release where every accumulated breaking change lands at once, behind one version bump, so you read one page instead of six.
Most applications need no code changes. The work below is mostly checking that a default which changed is still the default you want.
Before you start
Add the Nuxt peer dependency if your package manager is strict:
pnpm add -D nuxt@^4Frogger now declares compatibility: { nuxt: '^4.0.0' }, so an incompatible install fails at install time rather than at the first request.
Redaction actually happens now
If you use preset: 'standard' or preset: 'full', this is the change to read.
Both presets documented "redaction on" and resolved to a scrubber with zero rules. Anything you logged went out in plaintext while the config said otherwise.
They now seed RECOMMENDED_RULES: passwords/secrets/tokens are redacted, SSNs are pseudonymised, card and account numbers are masked, and emails, phone numbers, names and addresses are partially masked.
What to do: look at what your logs contain now. If a field you relied on reading is being masked, override it:
import { defineScrub, RECOMMENDED_RULES } from '#frogger/config'
scrub: defineScrub()
.use(...RECOMMENDED_RULES)
.never('supportEmail') // keep this one readable
.build()A bare scrub: true still injects no rules — turning the scrubber on and choosing a rule set are separate decisions. The build now warns when a scrubber resolves to zero rules, so the silent-no-op case is visible.
Frogger no longer takes over shutdown
Previously, standard and full installed SIGTERM/SIGINT handlers that drained for 3 seconds and then called process.exit(0), and forced process.exit(1) on an uncaught exception.
On a rolling deploy that truncated any in-flight request longer than 3 seconds, along with every other shutdown handler your app registered.
Frogger now relies on Nitro's close hook, which already drained the queue.
What to do: nothing, unless your deployment has no Nitro close path:
errorCapture: {
server: {
takeoverSignals: true, // restore the old SIGTERM/SIGINT behaviour
exitOnUncaught: true, // restore process.exit(1) on uncaughtException
drainTimeoutMs: 3000,
},
}On serverless or edge, use your platform's waitUntil equivalent instead of either.
Error capture no longer ships secrets
Three defaults flipped to false:
| Option | Was | Now | Why |
|---|---|---|---|
errorCapture.server.includeHeaders | true | false | Sent Cookie and Authorization verbatim |
errorCapture.client.includeComponentProps | true | false | Component props routinely carry PII and tokens |
errorCapture.client.includeComponentOuterHTML | true | false | Rendered markup is rendered user data, and unbounded |
What to do: turn back on what you actually want.
errorCapture: {
server: {
includeHeaders: ['user-agent', 'referer'], // an allow-list, or `true`
},
client: {
includeComponentProps: true,
},
}Even with includeHeaders: true, cookie, set-cookie, authorization, proxy-authorization, x-api-key, x-auth-token and x-csrf-token are replaced with [redacted] unconditionally. outerHTML is truncated to 4 KiB.
lvl values changed for verbose and silent
lvl was copied straight off consola, where silent is -Infinity and verbose is +Infinity — both of which JSON.stringify turns into null. Those rows reached every transport with a null level, and verbose() could not fire at any finite threshold at all.
| Type | Was | Now |
|---|---|---|
verbose | Infinity → null on the wire | 5 (the trace tier) |
silent | -Infinity → null on the wire | -1 |
Every level is now finite and JSON-safe. Rows also carry a new sev field: the OpenTelemetry SeverityNumber, derived from type.
What to do: if a downstream query filters on lvl === 999 or lvl === -999, update it. Prefer sev for new queries.
Rate limiting ignores forwarding headers by default
rateLimit trusted x-forwarded-for, x-real-ip, cf-connecting-ip and four others unconditionally. An attacker rotating x-real-ip got a fresh bucket per request — no limit at all — and one spoofing a victim's address could drive that address into the escalating block list for hours.
What to do: if you run behind a proxy or CDN, declare it.
rateLimit: {
trustProxy: true, // trust one hop of x-forwarded-for
// trustProxy: 2, // ...or two
// trustProxy: ['10.0.0.1'], // ...or only from these peer addresses
}Without this, limits key on the socket peer address, which behind a proxy is the proxy — every request shares one bucket.
The reporter and app limit tiers now only apply to authenticated requests, since both key on attacker-supplied headers.
parentId is now parentSpanId, and means something different
Span identity changed. Previously spanId was re-minted on every log call, so no two rows ever shared one, and parentId meant "the row emitted immediately before this one on this logger" — which is not a parent edge.
Now: each logger owns one stable span id for its lifetime. Every row it emits carries that span id, and parentSpanId names the span that created this one.
What to do: rename trace.parentId to trace.parentSpanId in any downstream query. "all the logs inside this span" is now a single predicate on trace.spanId, which was not expressible before.
Removed
LoggerObject.tagsand the.tags([...])live-log filter. The field was declared, documented and filterable, and written by no code path, so the filter matched nothing. Re-propose it if you want free-form tagging.- Websocket query scaffolding — the historical-query message types, the deduplicator,
reconnectSubscription, and themaxConcurrentQueries,maxQueryResults,defaultQueryTimeoutandcacheoptions. None were reachable; none were in the package exports map. public.serverModule— a documented option no code ever read. The top-levelserverModuleis the real one.setFroggerMetricsUser()is deprecated (removed in 0.3.0). Usefrogger.identify(), which sets the user for logs and metrics.
Wire format changes
All additive; meta.schema is now frogger.logs/1 and frogger.metrics/1. See the wire format reference.
meta.schemaon every batch — branch on this instead of sniffing fields.id(uuidv7) on every record — the dedupe and sort key.resourceon the envelope, denormalised onto rows:service.name,service.version,deployment.environment,service.release,service.instance.id.sevon every log row — the OTel SeverityNumber.obsTimeon every ingested row — when the collector observed it, as against thetimethe emitter claimed.session,userandrouteas top-level, never-scrubbed fields.spanson the log envelope — first-classSpanObjectrecords.kind: 'event'on rows fromfrogger.event().
Worth turning on
Nothing here is required, but 0.2.0 is where these became available:
export default defineFroggerOptions({
// frogger.debug() and frogger.trace() were process-wide no-ops with no way
// to enable them. Now there is one.
level: { server: 'debug', client: 'info' },
// Stamp which environment a row came from. Reads NUXT_FROGGER_ENVIRONMENT,
// so one build can be promoted across environments without a rebuild.
environment: 'production',
transports: [
// Works on every preset, including edge. No infrastructure.
stdoutTransport(),
// Warn and above to a remote sink, everything to the local file.
fileTransport(),
httpTransport({ url: '...', minLevel: 'warn' }),
// Or speak OTLP, which every OpenTelemetry backend accepts.
httpTransport({ url: 'https://collector/v1/logs', shape: 'otlp-logs' }),
],
metrics: {
requests: true, // per-route latency, status and error rate
runtime: true, // event-loop delay, GC pause, heap
},
// Keep 10% of traces, and every trace containing an error.
sampling: { rate: 0.1 },
})And to check the pipeline is healthy:
import { getFroggerHealth } from 'nuxt-frogger/transport'
const { enqueued, delivered, dropped } = getFroggerHealth()