Error reporting with Sentry (sentry)
Reports what breaks to Sentry without the SDK: every unhandled error that becomes a 5xx, and failed background work (event listeners, app.background() tasks). Events carry the stack trace, the route, the URL without query secrets, the user id, the request's earlier log lines as breadcrumbs, the release and the environment. Credentials, cookies, passwords and tokens are scrubbed before anything leaves the server. 4xx are ignored, identical errors are sent once per window, and a sample rate and a per minute cap protect your quota. No dependencies, no change to the core: the block listens to the logger.
npx calm add sentry| Adds | |
|---|---|
| Files | app/blocks/sentry/index.js (what is captured, the log sink), event.js (DSN, stack frames, scrubbing, envelopes) |
| Environment | SENTRY_DSN (required in production), SENTRY_ENVIRONMENT, SENTRY_RELEASE |
Setup in Sentry#
- Create a project (Projects, Create project, platform Node.js).
- Copy its DSN: Project settings, Client Keys (DSN). It looks like
https://<key>@o123.ingest.sentry.io/456. - In
.env:
SENTRY_DSN=https://<key>@o123.ingest.sentry.io/456
SENTRY_RELEASE=api@1.4.0 # optional, default: <package name>@<version>
SENTRY_ENVIRONMENT=staging # optional, default: NODE_ENVAn invalid DSN is reported by calm doctor; a missing one is an error in production.
What is reported#
| Source | Reported | Details |
|---|---|---|
| An error thrown in a route, service or hook that answers 5xx | yes | stack, transaction (POST /api/orders/:id), request, user id, breadcrumbs, status_code tag |
| 4xx (validation, 401, 403, 404, 429) | never | the client's mistake, not a bug |
log.error(msg, { err }) elsewhere (failed app.background() tasks, event listeners) | yes (captureLogErrors) | tag source: log, extra.log is the log message |
| Your own calls | yes | captureException, captureMessage |
// Report a handled problem
ctx.app.ext.sentry.captureException(err, { tags: { feature: 'checkout' }, extra: { orderId } });
ctx.app.ext.sentry.captureMessage('Payment provider slow', { level: 'warning' });Both return the event id, or null when the event was ignored, deduplicated, sampled out or capped. err.cause chains are sent as chained exceptions.
Scrubbing#
- Request headers: only an allowlist is sent (
user-agent,content-type,accept,referer,origin,host...).AuthorizationandCookienever are. - The URL is sent without its query string; the query string is sent separately with sensitive values replaced by
[Filtered](?token=...,?code=...,?signature=...). - Bodies (only with
sendBody: true) andextraare scrubbed recursively: any key containingpassword,secret,token,authorization,cookie,session,apiKey,privateKey,credential,signature,totp,recoveryCode... or ending withcode,otp,card,cvv,ssn. Add your own withsensitiveKeys. - Messages and breadcrumbs:
Bearer .../Basic ...values, CalmAPI tokens and secret query parameters are replaced. - The user is
{ id }only. Email (sendUserEmail) and IP (sendIp) are opt-in.
Options#
export default defineConfig({
blocks: {
sentry: {
sampleRate: 1, // 0 to 1
dedupeSeconds: 60, // the same error (type, message, place in your code) once per window
maxEventsPerMinute: 30, // per instance
maxBreadcrumbs: 20, // log lines of the request (0: none)
sendUserEmail: false,
sendIp: false,
sendBody: false,
captureLogErrors: true,
sensitiveKeys: ['iban', 'taxId'],
ignoreErrors: ['ECONNRESET', /^Client closed/],
beforeSend: (event) => (event.tags.route === 'GET /api/health' ? null : event),
},
},
});After a 429 from Sentry, sending pauses for the Retry-After it asks for. app.ext.sentry.stats counts sent, deduplicated, sampled out, capped, ignored and failed events (also in devtools, Sentry panel).
Protocol#
Events go to https://<host>/api/<project>/envelope/ as an envelope (application/x-sentry-envelope: envelope header, item header, event), with X-Sentry-Auth: Sentry sentry_version=7, sentry_key=<public key>, sentry_client=calmapi-sentry/1.0.0. Only the public key of the DSN is used.
Security notes#
- Error reports leave your infrastructure: review what you add to
extraandtags, they are scrubbed by key name only. - Self-hosted Sentry works the same (the DSN points to it). Keep it on https.
- The block reports its own sending problems with
log.warn, never as errors, so a Sentry outage cannot loop.
Official documentation#
Read these before changing the block; they are also in app/blocks/sentry/AGENTS.md and in the devtools Blocks panel.
- Sentry envelopes
- Sentry event payloads
- Sentry SDK overview (authentication, DSN)
- Data Source Name (DSN)
- Scrubbing sensitive data
For AI agents#
Coding agents read these notes in app/blocks/sentry/AGENTS.md:
- Read
app/blocks/sentry/index.js(capture rules, the log sink) andevent.js(DSN, stack frames, scrubbing, envelope) before changing anything. - Errors reach Sentry through the logger: the core error handler logs 5xx with
log.error(msg, { err })after settingreq.calmError. Do not add try/catch blocks that swallow errors to report them; let them throw. - Invariant: 4xx are never reported, and nothing leaves the server unscrubbed. Every new field added to an event must go through
scrub()orscrubString(). - Invariant: only the user id is sent by default. Email (
sendUserEmail), IP (sendIp) and request bodies (sendBody) are opt-in options; do not turn them on without the user asking. - Add keys to scrub with the
sensitiveKeysoption in calm.config.js rather than editing the built-in list. - Report handled problems with
ctx.app.ext.sentry.captureException(err, { tags, extra })orcaptureMessage(text, { level }); both return the event id or null. SENTRY_DSNlives in.env. The DSN's public key is not a secret, but keep it out of git anyway.- Inside this block, log failures with
log.warnand never with anerrfield at error level: the sink would report its own failures in a loop. - Noise control lives in
admit(): dedupe window, per minute cap, sample rate, and a pause after Sentry answers 429. Keep it in front of every send. - Tests point
SENTRY_DSNat a local node:http server (http://key@127.0.0.1:port/42) and read the envelopes it receives.