Logging

There is one logging API on each side of the wire. On the client it is log from @tinycld/core/lib/logger; on the server it is logging.ForPackage from tinycld.org/core/logging. Both attach the right context for Sentry so a report can be traced back to a package and a user. Neither is console.*, which Biome bans in runtime code (suspicious/noConsole is an error; only scripts, tests, and a short allowlist inside core may use it).

Client: log

import { log } from '@tinycld/core/lib/logger'

export function useImport() {
    const run = async (filename: string) => {
        log.debug('example.import', 'starting', { filename })
        try {
            await pb.collection('example').create({ filename })
        } catch (err) {
            log.error('example.import.create', err, { filename })
            throw err
        }
    }
    return { run }
}

Four levels, one shape:

CallArguments
log.debug(context, message, extra?)dev tracing
log.info(context, message, extra?)routine events worth a breadcrumb
log.warn(context, message, extra?)something off that a person should see before it escalates
log.error(context, error, extra?)a caught failure — pass the error object, never a string

context is a short, stable, dotted string Sentry groups on — pick something specific (mail.openDraft.fetchBody, not mail.error) and never interpolate user data into it. extra is the bag for variable detail; Sentry’s scrubbing runs on it.

What a call does:

captureException from @tinycld/core/lib/errors still exists and is an alias for log.error; existing code using it needs no change.

Form validation errors

When a useMutation fails because PocketBase rejected a field, the right handler is handleMutationErrorsWithForm({ setError, getValues }) from @tinycld/core/lib/errors. It maps validation errors back onto the react-hook-form fields and routes everything else into a root error you can render with <FormErrorSummary />. See Forms.

Don’t combine it with log.error — a validation failure is not a bug.

Server: logging.ForPackage

import "tinycld.org/core/logging"

var log = logging.ForPackage("example")

func flush(ctx context.Context, id string) {
    log.WarnContext(ctx, "refusing to flush a card from another board", "cardID", id)
}

ForPackage(slug) returns a *slog.Logger stamped with a pkg attribute, so there is no need for hand-written "example: " message prefixes. Records fan out to three sinks:

SinkLevel
stderrinfo and above
the PocketBase _logs tablewhatever the admin dashboard’s log level is set to
Sentrywarn and above

Prefer the *Context variants (InfoContext, WarnContext, ErrorContext) whenever a ctx is in scope: the per-request Sentry hub carries the user id, so those calls get user attribution for free. A call without a ctx still logs and still reaches Sentry, just unattributed. Do not add a ctx parameter to a function solely to log.

What not to do